/*
 * 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.points;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.ListIterator;

import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.AbstractInsnNode;
import org.objectweb.asm.tree.InsnList;
import org.objectweb.asm.tree.MethodInsnNode;
import org.objectweb.asm.tree.TypeInsnNode;
import org.spongepowered.asm.mixin.injection.InjectionPoint;
import org.spongepowered.asm.mixin.injection.InjectionPoint.AtCode;
import org.spongepowered.asm.mixin.injection.selectors.ITargetSelector;
import org.spongepowered.asm.mixin.injection.selectors.ITargetSelectorConstructor;
import org.spongepowered.asm.mixin.injection.selectors.TargetSelector;
import org.spongepowered.asm.mixin.injection.struct.InjectionPointData;
import org.spongepowered.asm.mixin.injection.throwables.InvalidInjectionPointException;
import org.spongepowered.asm.util.Constants;

import com.google.common.base.Strings;

/**
 * <p>This injection point searches for NEW opcodes matching its arguments and
 * returns a list of insns immediately prior to matching instructions. It
 * accepts the following parameters from
 * {@link org.spongepowered.asm.mixin.injection.At At}:</p>
 * 
 * <dl>
 *   <dt><i>named argument:</i> class (or specify using <tt>target</tt></dt>
 *   <dd>The value of the NEW node to look for, the fully-qualified class name
 *   </dd>
 *   <dt>ordinal</dt>
 *   <dd>The ordinal position of the NEW opcode to match. For example if the NEW
 *   opcode appears 3 times in the method and you want to match the 3rd then you
 *   can specify an <em>ordinal</em> of <b>2</b> (ordinals are zero-indexed).
 *   The default value is <b>-1</b> which supresses ordinal matching</dd>
 *   <dt>target</dt>
 *   <dd>Target class can also be specified in <tt>target</tt> which also
 *   supports specifying the exact signature of the constructor to target. In
 *   this case the <em>target type</em> is specified as the return type of the
 *   constructor (in place of the usual <tt>V</tt> (void)) and no owner or name
 *   should be specified (they are ignored).</dd>
 * </dl>
 * 
 * <p>Examples:</p>
 * <blockquote><pre>
 *   <del>// Find all NEW opcodes for <tt>String</tt></del>
 *   &#064;At(value = "NEW", args = "class=java/lang/String")</pre>
 * </blockquote> 
 * <blockquote><pre>
 *   <del>// Find all NEW opcodes for <tt>String</tt></del>
 *   &#064;At(value = "NEW", target = "java/lang/String"</pre>
 * </blockquote> 
 * <blockquote><pre>
 *   <del>// Find all NEW opcodes for <tt>String</tt> which are constructed
 *   // using the ctor which takes an array of <tt>char</tt></del>
 *   &#064;At(value = "NEW", target = "([C)Ljava/lang/String;"</pre>
 * </blockquote> 
 * 
 * <p>Note that like all standard injection points, this class matches the insn
 * itself, putting the injection point immediately <em>before</em> the access in
 * question. Use {@link org.spongepowered.asm.mixin.injection.At#shift shift}
 * specifier to adjust the matched opcode as necessary.</p>
 */
@AtCode("NEW")
public class BeforeNew extends InjectionPoint {

    /**
     * Class name we're looking for
     */
    private final String target;
    
    /**
     * Ctor descriptor we're looking for 
     */
    private final String desc;

    /**
     * Ordinal value
     */
    private final int ordinal;

    public BeforeNew(InjectionPointData data) {
        super(data);
        
        this.ordinal = data.getOrdinal();
        String target = Strings.emptyToNull(data.get("class", data.get("target", "")).replace('.', '/'));
        ITargetSelector member = TargetSelector.parseAndValidate(target, data.getContext());
        if (!(member instanceof ITargetSelectorConstructor)) {
            throw new InvalidInjectionPointException(data.getMixin(), "Failed parsing @At(\"NEW\") target descriptor \"%s\" on %s",
                    target, data.getDescription());
        }
        ITargetSelectorConstructor targetSelector = (ITargetSelectorConstructor)member;
        this.target = targetSelector.toCtorType();
        this.desc = targetSelector.toCtorDesc();
    }
    
    /**
     * Returns whether this injection point has a constructor descriptor defined
     */
    public boolean hasDescriptor() {
        return this.desc != null;
    }
    
    /**
     * Gets the descriptor from the injection point, can return null
     */
    public String getDescriptor() {
        return this.desc;
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean find(String desc, InsnList insns, Collection<AbstractInsnNode> nodes) {
        boolean found = false;
        int ordinal = 0;

        Collection<TypeInsnNode> newNodes = new ArrayList<TypeInsnNode>();
        Collection<AbstractInsnNode> candidates = (Collection<AbstractInsnNode>) (this.desc != null ? newNodes : nodes);
        ListIterator<AbstractInsnNode> iter = insns.iterator();
        while (iter.hasNext()) {
            AbstractInsnNode insn = iter.next();

            if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW && this.matchesOwner((TypeInsnNode) insn)) {
                if (this.ordinal == -1 || this.ordinal == ordinal) {
                    candidates.add(insn);
                    found = this.desc == null;
                }

                ordinal++;
            }
        }
        
        if (this.desc != null) {
            for (TypeInsnNode newNode : newNodes) {
                if (BeforeNew.findInitNodeFor(insns, newNode, this.desc) != null) {
                    nodes.add(newNode);
                    found = true;
                }
            }
        }

        return found;
    }

    public static MethodInsnNode findInitNodeFor(InsnList insns, TypeInsnNode newNode, String desc) {
        int indexOf = insns.indexOf(newNode);
        int depth = 0;
        for (Iterator<AbstractInsnNode> iter = insns.iterator(indexOf); iter.hasNext();) {
            AbstractInsnNode insn = iter.next();
            if (insn instanceof MethodInsnNode && insn.getOpcode() == Opcodes.INVOKESPECIAL) {
                MethodInsnNode methodNode = (MethodInsnNode)insn;
                if (Constants.CTOR.equals(methodNode.name) && --depth == 0) {
                    return methodNode.owner.equals(newNode.desc) && (desc == null || methodNode.desc.equals(desc)) ? methodNode : null;
                }
            } else if (insn instanceof TypeInsnNode && insn.getOpcode() == Opcodes.NEW) {
                depth++;
            }
        }
        return null;
    }

    private boolean matchesOwner(TypeInsnNode insn) {
        return this.target == null || this.target.equals(insn.desc);
    }
}
