/*
 * This file is part of Mixin, licensed under the MIT License (MIT).
 *
 * Copyright (c) SpongePowered <https://www.spongepowered.org>
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.spongepowered.asm.mixin.injection.code;

import java.util.Deque;
import java.util.LinkedList;
import java.util.ListIterator;
import java.util.NoSuchElementException;

import org.spongepowered.asm.logging.ILogger;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.AnnotationNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodNode;
import org.spongepowered.asm.mixin.MixinEnvironment.Option;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import org.spongepowered.asm.mixin.injection.InjectionPoint.Specifier;
import org.spongepowered.asm.mixin.injection.Slice;
import org.spongepowered.asm.mixin.injection.struct.InjectionPointAnnotationContext;
import org.spongepowered.asm.mixin.injection.struct.Target;
import org.spongepowered.asm.mixin.injection.throwables.InjectionError;
import org.spongepowered.asm.mixin.injection.throwables.InvalidSliceException;
import org.spongepowered.asm.service.MixinService;
import org.spongepowered.asm.util.Annotations;

import com.google.common.base.Strings;

/**
 * Stores information about a defined method slice for a particular injector.
 */
public final class MethodSlice {
    
    /**
     * A read-only wrapper for an {@link InsnList} which only allows the segment
     * identified by <tt>start</tt> and <tt>end</tt> to be accessed. In essence
     * this class provides a <em>view</em> of the underlying InsnList.
     */
    static final class InsnListSlice extends InsnListEx { 
    
        /**
         * ListIterator for the slice view, wraps an iterator returned by the
         * underlying {@link InsnList} and ensures that consumers can only
         * traverse the slice.
         * 
         * <p>Note that this doesn't handle changes in the underlying InsnList
         * which occur after instatiation, care should be taken not to modify
         * the list via other means whilst this iterator is in use.</p>
         */
        static class SliceIterator implements ListIterator<AbstractInsnNode> {
            
            /**
             * The underlying ListIterator returned by the parent list
             */
            private final ListIterator<AbstractInsnNode> iter;
            
            /**
             * Brackets
             */
            private int start, end;
            
            /**
             * Virtual index, used to keep track of bounds
             */
            private int index;
    
            public SliceIterator(ListIterator<AbstractInsnNode> iter, int start, int end, int index) {
                this.iter = iter;
                this.start = start;
                this.end = end;
                this.index = index;
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#hasNext()
             */
            @Override
            public boolean hasNext() {
                return this.index <= this.end && this.iter.hasNext();
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#next()
             */
            @Override
            public AbstractInsnNode next() {
                if (this.index > this.end) {
                    throw new NoSuchElementException();
                }
                this.index++;
                return this.iter.next();
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#hasPrevious()
             */
            @Override
            public boolean hasPrevious() {
                return this.index > this.start ;
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#previous()
             */
            @Override
            public AbstractInsnNode previous() {
                if (this.index <= this.start) {
                    throw new NoSuchElementException();
                }
                this.index--;
                return this.iter.previous();
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#nextIndex()
             */
            @Override
            public int nextIndex() {
                return this.index - this.start;
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#previousIndex()
             */
            @Override
            public int previousIndex() {
                return this.index - this.start - 1;
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#remove()
             */
            @Override
            public void remove() {
                throw new UnsupportedOperationException("Cannot remove insn from slice");
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#set(java.lang.Object)
             */
            @Override
            public void set(AbstractInsnNode e) {
                throw new UnsupportedOperationException("Cannot set insn using slice");
            }
    
            /* (non-Javadoc)
             * @see java.util.ListIterator#add(java.lang.Object)
             */
            @Override
            public void add(AbstractInsnNode e) {
                throw new UnsupportedOperationException("Cannot add insn using slice");
            }
        }
    
        /**
         * Brackets
         */
        private final int start, end;
        
        protected InsnListSlice(Target target, int start, int end) {
            super(target);
            
            // Start and end are validated prior to construction
            this.start = start;
            this.end = end;
        }
        
        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #iterator()
         */
        @Override
        public ListIterator<AbstractInsnNode> iterator() {
            return this.iterator(0);
        }
        
        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #iterator(int)
         */
        @Override
        public ListIterator<AbstractInsnNode> iterator(int index) {
            // Return the bracketed iterator
            return new SliceIterator(super.iterator(this.start + index), this.start, this.end, this.start + index);
        }

        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #toArray()
         */
        @Override
        public AbstractInsnNode[] toArray() {
            AbstractInsnNode[] all = super.toArray();
            AbstractInsnNode[] subset = new AbstractInsnNode[this.size()];
            System.arraycopy(all, this.start, subset, 0, subset.length);
            return subset;
        }

        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #size()
         */
        @Override
        public int size() {
            return (this.end - this.start) + 1;
        }

        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #getFirst()
         */
        @Override
        public AbstractInsnNode getFirst() {
            return super.get(this.start);
        }

        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #getLast()
         */
        @Override
        public AbstractInsnNode getLast() {
            return super.get(this.end);
        }

        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #get(int)
         */
        @Override
        public AbstractInsnNode get(int index) {
            return super.get(this.start + index);
        }

        /* (non-Javadoc)
         * @see org.spongepowered.asm.mixin.injection.code.ReadOnlyInsnList
         *      #contains(org.objectweb.asm.tree.AbstractInsnNode)
         */
        @Override
        public boolean contains(AbstractInsnNode insn) {
            if (insn == null) {
                return false;
            }
            for (AbstractInsnNode node : this.toArray()) {
                if (node == insn) {
                    return true;
                }
            }
            return false;
        }

        /**
         * Returns the index of the specified instruction in the slice, use
         * {@link #realIndexOf} to determine the index of the instruction in the
         * underlying InsnList.
         * 
         * @param insn Instruction to inspect
         * @return instruction's index in the list
         */
        @Override
        public int indexOf(AbstractInsnNode insn) {
            int index = super.indexOf(insn);
            return index >= this.start && index <= this.end ? index - this.start : -1;
        }
        
        /**
         * Returns the index of the instruction in the underlying InsnLis
         * 
         * @param insn Instruction to inspect
         * @return instruction's index in the list
         */
        public int realIndexOf(AbstractInsnNode insn) {
            return super.indexOf(insn);
        }
        
    }
    
    /**
     * Make with the logging already
     */
    private static final ILogger logger = MixinService.getService().getLogger("mixin");

    /**
     * Owner of this slice
     */
    private final ISliceContext owner;
    
    /**
     * Slice ID as declared, slice ID in the parent {@link MethodSlices}
     * collection may be different
     */
    private final String id;
    
    /**
     * Injection point which defines the start of this slice (inclusive), may be
     * null as long as {@link #to} is not null
     */
    private final InjectionPoint from;
    
    /**
     * Injection point which defines the end of this slice (inclusive), may be
     * null as long as {@link #from} is not null
     */
    private final InjectionPoint to;
    
    /**
     * Descriptive name of the slice, used in exceptions
     */
    private final String name;
    
    /**
     * Success counts for from and to injection points 
     */
    private int successCountFrom, successCountTo;

    /**
     * ctor
     * 
     * @param owner owner
     * @param id declared id
     * @param from start point, may be null as long as {@link #to} is not null
     * @param to end point, may be null as long as {@link #from} is not null
     */
    private MethodSlice(ISliceContext owner, String id, InjectionPoint from, InjectionPoint to) {
        if (from == null && to == null) {
            throw new InvalidSliceException(owner, String.format("%s is redundant. No 'from' or 'to' value specified", this));
        }

        this.owner = owner;
        this.id = Strings.nullToEmpty(id);
        this.from = from;
        this.to = to;
        this.name = MethodSlice.getSliceName(id);
    }
    
    /**
     * Get the <em>declared</em> id of this slice 
     */
    public String getId() {
        return this.id;
    }
    
    /**
     * Get a sliced insn list based on the parameters specified in this slice
     * 
     * @param target method to slice
     * @return read only slice
     */
    public InsnListReadOnly getSlice(Target target) {
        int max = target.insns.size() - 1;
        int start = this.find(target, this.from, 0, 0, "from");
        int end = this.find(target, this.to, max, start, "to");
        
        if (start > end) {
            throw new InvalidSliceException(this.owner, String.format("%s is negative size. Range(%d -> %d)", this.describe(), start, end));
        }
        
        if (start < 0 || end < 0 || start > max || end > max) {
            throw new InjectionError("Unexpected critical error in " + this + ": out of bounds start=" + start + " end=" + end + " lim=" + max);
        }
        
        if (start == 0 && end == max) {
            return new InsnListEx(target);
        }
        
        return new InsnListSlice(target, start, end);
    }

    /**
     * Runs the specified injection point as a query on the method and returns
     * the index of the instruction matching the query. Returns the default
     * value if the query returns zero results.
     * 
     * @param target Method to query
     * @param injectionPoint Query to run
     * @param defaultValue Value to return if injection point is null (open
     *      ended)
     * @param failValue Value to use if query fails
     * @param argument The name of the argument ("from" or "to") for debug msgs
     * @return matching insn index
     */
    private int find(Target target, InjectionPoint injectionPoint, int defaultValue, int failValue, String argument) {
        if (injectionPoint == null) {
            return defaultValue;
        }
        
        String description = String.format("%s(%s)", this.name, argument);
        Deque<AbstractInsnNode> nodes = new LinkedList<AbstractInsnNode>();
        InsnList insns = new InsnListEx(target);
        boolean result = injectionPoint.find(target.getDesc(), insns, nodes);
        Specifier specifier = injectionPoint.getSpecifier(Specifier.FIRST);
        if (specifier == Specifier.ALL) {
            throw new InvalidSliceException(this.owner, String.format("ALL is not a valid specifier for slice %s", this.describe(description)));
        }
        if (nodes.size() != 1 && specifier == Specifier.ONE) {
            throw new InvalidSliceException(this.owner, String.format("%s requires 1 result but found %d", this.describe(description), nodes.size()));
        }
        
        if (!result) {
            return failValue;
        }
        
        if ("from".equals(argument)) {
            this.successCountFrom++;
        } else {
            this.successCountTo++;
        }
        
        return target.indexOf(specifier == Specifier.FIRST ? nodes.getFirst() : nodes.getLast());
    }

    /**
     * Perform post-injection debugging and validation tasks
     */
    public void postInject() {
        if (this.owner.getMixin().getOption(Option.DEBUG_VERBOSE)) {
            if (this.from != null && this.successCountFrom == 0) {
                MethodSlice.logger.warn("{} did not match any instructions", this.describe(this.name + "(from)"));
            }
            if (this.to != null && this.successCountTo == 0) {
                MethodSlice.logger.warn("{} did not match any instructions", this.describe(this.name + "(to)"));
            }
        }
    }

    /* (non-Javadoc)
     * @see java.lang.Object#toString()
     */
    @Override
    public String toString() {
        return this.describe();
    }

    private String describe() {
        return this.describe(this.name);
    }

    private String describe(String description) {
        return MethodSlice.describeSlice(description, this.owner);
    }

    private static String describeSlice(String description, ISliceContext owner) {
        String annotation = Annotations.getSimpleName(owner.getAnnotationNode());
        MethodNode method = owner.getMethod();
        return String.format("%s->%s(%s)::%s%s", owner.getMixin(), annotation, description, method.name, method.desc);
    }

    private static String getSliceName(String id) {
        return String.format("@Slice[%s]", Strings.nullToEmpty(id));
    }

    /**
     * Parses the supplied annotation into a MethodSlice
     * 
     * @param owner Owner injection info
     * @param slice Annotation to parse
     * @return parsed MethodSlice
     */
    public static MethodSlice parse(ISliceContext owner, Slice slice) {
        String id = slice.id();
        
        At from = slice.from();
        At to = slice.to();
        
        InjectionPoint fromPoint = from != null ? InjectionPoint.parse(owner, from) : null;
        InjectionPoint toPoint = to != null ? InjectionPoint.parse(owner, to) : null;
        
        return new MethodSlice(owner, id, fromPoint, toPoint);
    }

    /**
     * Parses the supplied annotation into a MethodSlice
     * 
     * @param info Owner injection info
     * @param node Annotation to parse
     * @return parsed MethodSlice
     */
    public static MethodSlice parse(ISliceContext info, AnnotationNode node) {
        String id = Annotations.<String>getValue(node, "id");
        String coord = "slice";
        if (!Strings.isNullOrEmpty(id)) {
            coord += "." + id;
        }
        InjectionPointAnnotationContext sliceContext = new InjectionPointAnnotationContext(info, node, coord);
        
        AnnotationNode from = Annotations.<AnnotationNode>getValue(node, "from");
        AnnotationNode to = Annotations.<AnnotationNode>getValue(node, "to");
        
        InjectionPoint fromPoint = from != null ? InjectionPoint.parse(new InjectionPointAnnotationContext(sliceContext, from, "from"), from) : null;
        InjectionPoint toPoint = to != null ? InjectionPoint.parse(new InjectionPointAnnotationContext(sliceContext, to, "to"), to) : null;
        
        return new MethodSlice(info, id, fromPoint, toPoint);
    }

}
