/*
 *  Copyright (C) 2010-2021 JPEXS, All rights reserved.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.jpexs.decompiler.flash.abc.avm2.fastavm2;

import com.jpexs.decompiler.flash.abc.avm2.AVM2Code;
import com.jpexs.decompiler.flash.abc.avm2.instructions.AVM2Instruction;
import com.jpexs.decompiler.flash.abc.avm2.instructions.IfTypeIns;
import com.jpexs.decompiler.flash.abc.avm2.instructions.jumps.JumpIns;
import com.jpexs.decompiler.flash.abc.avm2.instructions.jumps.LookupSwitchIns;
import com.jpexs.decompiler.flash.abc.types.ABCException;
import com.jpexs.decompiler.flash.abc.types.MethodBody;
import com.jpexs.decompiler.graph.GraphSourceItemContainer;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 *
 * @author JPEXS
 */
public class FastAVM2List implements Collection<AVM2InstructionItem> {

    private int size;

    private AVM2InstructionItem firstItem;

    private final Map<AVM2Instruction, AVM2InstructionItem> actionItemMap;

    private final Set<AVM2InstructionItem> actionItemSet;

    public FastAVM2List(MethodBody body) {
        // exceptions todo
        AVM2Code avm2code = body.getCode();
        List<AVM2Instruction> code = avm2code.code;
        actionItemMap = new HashMap<>(code.size());
        actionItemSet = new HashSet<>(code.size());
        for (AVM2Instruction action : code) {
            insertItemAfter(null, action);
        }

        size = code.size();
//        getContainerLastActions(avm2code, actionItemMap);
        getJumps(avm2code, actionItemMap);
    }

    public final AVM2InstructionItem insertItemBefore(AVM2InstructionItem item, AVM2Instruction action) {
        AVM2InstructionItem newItem = new AVM2InstructionItem(action);
        return insertItemBefore(item, newItem);
    }

    public final AVM2InstructionItem insertItemAfter(AVM2InstructionItem item, AVM2Instruction action) {
        AVM2InstructionItem newItem = new AVM2InstructionItem(action);
        return insertItemAfter(item, newItem);
    }

    public final AVM2InstructionItem insertItemBefore(AVM2InstructionItem item, AVM2InstructionItem newItem) {
        insertItemAfter(item.prev, newItem);
        if (item == firstItem) {
            firstItem = newItem;
        }

        return newItem;
    }

    public final AVM2InstructionItem insertItemAfter(AVM2InstructionItem item, AVM2InstructionItem newItem) {
        if (item == null && firstItem == null) {
            firstItem = newItem;
            newItem.next = newItem;
            newItem.prev = newItem;
        } else {
            if (item == null) {
                // insert to the end
                item = firstItem.prev;
            }

            AVM2InstructionItem oldNext = item.next;
            newItem.prev = item;
            newItem.next = oldNext;
            item.next = newItem;
            oldNext.prev = newItem;
        }

        size++;
        actionItemMap.put(newItem.ins, newItem);
        actionItemSet.add(newItem);
        return newItem;
    }

    public AVM2InstructionItem removeItem(AVM2InstructionItem item) {
        AVM2InstructionItem next = null;
        if (item == firstItem) {
            if (item.next == item) {
                // there is only 1 item
                firstItem = null;
            } else {
                next = item.next;
                firstItem = next;
                next.prev = item.prev;
                item.prev.next = next;
            }
        } else {
            next = item.next;
            item.prev.next = next;
            next.prev = item.prev;
        }

        size--;
        actionItemMap.remove(item.ins);
        actionItemSet.remove(item);

        item.removeJumpTarget();
        item.removeContainerLastInstructions();

        if (item.jumpsHere != null) {
            for (AVM2InstructionItem item1 : new ArrayList<>(item.jumpsHere)) {
                item1.setJumpTarget(item.next);
            }
        }

        if (item.lastInsOf != null) {
            for (AVM2InstructionItem item1 : new ArrayList<>(item.lastInsOf)) {
                item1.replaceContainerLastInstruction(item, item.prev);
            }
        }

        return next;
    }

    public void removeItem(int index, int count) {
        FastAVM2ListIterator iterator = new FastAVM2ListIterator(this, index);
        for (int i = 0; i < count; i++) {
            iterator.next();
            iterator.remove();
        }
    }

    public AVM2InstructionItem get(int index) {
        FastAVM2ListIterator iterator = new FastAVM2ListIterator(this, index);
        return iterator.next();
    }

    public void replaceJumpTargets(AVM2InstructionItem target, AVM2InstructionItem newTarget) {
        if (target.jumpsHere != null) {
            for (AVM2InstructionItem item : new ArrayList<>(target.jumpsHere)) {
                item.setJumpTarget(newTarget);
            }
        }
    }

//    private void getContainerLastActions(AVM2Code actions, Map<AVM2Instruction, AVM2InstructionItem> actionItemMap) {
//        AVM2InstructionItem item = firstItem;
//        if (item == null) {
//            return;
//        }
//
//        do {
//            AVM2Instruction action = item.ins;
//            if (action instanceof GraphSourceItemContainer) {
//                item.setContainerLastInstructions(getContainerLastActions(actions, action, actionItemMap));
//            }
//
//            item = item.next;
//        } while (item != firstItem);
//    }
//    private List<AVM2InstructionItem> getContainerLastActions(AVM2Code actions, AVM2Instruction action, Map<AVM2Instruction, AVM2InstructionItem> actionItemMap) {
//        GraphSourceItemContainer container = (GraphSourceItemContainer) action;
//        List<Long> sizes = container.getContainerSizes();
//        long endAddress = action.getAddress() + container.getHeaderSize();
//        List<AVM2InstructionItem> lasts = new ArrayList<>(sizes.size());
//        for (long size : sizes) {
//            endAddress += size;
//            long lastActionAddress = getNearAddress(actions.code, endAddress - 1, false);
//            AVM2Instruction lastAction = null;
//            if (lastActionAddress != -1) {
//                lastAction = actions.getByAddress(lastActionAddress);
//            }
//
//            if (lastAction != null) {
//                lasts.add(actionItemMap.get(lastAction));
//            } else {
//                lasts.add(null);
//            }
//        }
//        return lasts;
//    }
    private long getNearAddress(List<AVM2Instruction> instructions, long address, boolean next) {
        int min = 0;
        int max = instructions.size() - 1;

        while (max >= min) {
            int mid = (min + max) / 2;
            long midValue = instructions.get(mid).getAddress();
            if (midValue == address) {
                return address;
            } else if (midValue < address) {
                min = mid + 1;
            } else {
                max = mid - 1;
            }
        }

        return next
                ? (min < instructions.size() ? instructions.get(min).getAddress() : -1)
                : (max >= 0 ? instructions.get(max).getAddress() : -1);
    }

    private void getJumps(AVM2Code actions, Map<AVM2Instruction, AVM2InstructionItem> actionItemMap) {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            AVM2Instruction action = item.ins;
            long target = -1;
            if (action.definition instanceof IfTypeIns) {
                target = action.getTargetAddress();
            } else if (action.definition instanceof LookupSwitchIns) {
                // todo
            }
            if (target >= 0) {
                AVM2Instruction targetAction = actions.adr2ins(target);
                item.setJumpTarget(actionItemMap.get(targetAction));
            }

            item = item.next;
        } while (item != firstItem);
    }

    private void updateActionAddressesAndLengths() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        long offset = item.ins.getAddress();
        do {
            AVM2Instruction action = item.ins;
            action.setAddress(offset);
            offset += action.getBytesLength();
            item = item.next;
        } while (item != firstItem);
    }

    private void updateJumps() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        long endAddress = item.prev.ins.getAddress();
        do {
            AVM2Instruction action = item.ins;
            if (action.definition instanceof IfTypeIns) {
                AVM2Instruction target = item.getJumpTargetInstruction();
                long offset;
                if (target != null) {
                    offset = target.getAddress() - action.getAddress() - action.getBytesLength();
                } else {
                    offset = endAddress - action.getAddress() - action.getBytesLength();
                }
                action.setTargetOffset((int) offset);
            } else if (action.definition instanceof LookupSwitchIns) {
                // todo
            }

            item = item.next;
        } while (item != firstItem);
    }

    private void updateContainerSizes() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            AVM2Instruction action = item.ins;
            if (action instanceof GraphSourceItemContainer) {
                GraphSourceItemContainer container = (GraphSourceItemContainer) action;
                List<AVM2InstructionItem> lastActions = item.getContainerLastInstructions();
                long startAddress = action.getAddress() + container.getHeaderSize();
                for (int j = 0; j < lastActions.size(); j++) {
                    AVM2Instruction lastAction = lastActions.get(j).ins;
                    int length = (int) (lastAction.getAddress() + lastAction.getBytesLength() - startAddress);
                    container.setContainerSize(j, length);
                    startAddress += length;
                }
            }

            item = item.next;
        } while (item != firstItem);
    }

    public AVM2InstructionItem getContainer(AVM2InstructionItem item) {
        while (!(item.ins instanceof GraphSourceItemContainer) && item != firstItem) {
            item = item.prev;
        }

        if (item.ins instanceof GraphSourceItemContainer) {
            return item;
        }

        return null;
    }

    public void removeZeroJumps() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            AVM2Instruction ins = item.ins;
            if (ins.definition instanceof JumpIns) {
                if (item.getJumpTarget() == item.next && item.getJumpTarget() != firstItem) {
                    item = removeItem(item);
                    continue;
                }
            }

            item = item.next;
        } while (item != firstItem);
    }

    public void removeUnreachableActions() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        updateReachableFlags(null, null);

        do {
            if (item.reachable == 0) {
                item = removeItem(item);
                continue;
            }

            item = item.next;
        } while (item != firstItem);
    }

    public void removeIncludedActions() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            if (!item.excluded) {
                item = removeItem(item);
                continue;
            }

            item = item.next;
        } while (item != firstItem);
    }

    public int getUnreachableActionCount(AVM2InstructionItem jump, AVM2InstructionItem jumpTarget) {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return 0;
        }

        updateReachableFlags(jump, jumpTarget);
        jump.reachable = 0;

        int count = 0;
        do {
            if (item.reachable == 0) {
                count++;
            }

            item = item.next;
        } while (item != firstItem);

        return count;
    }

    private void clearReachableFlags() {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            item.reachable = 0;
            item = item.next;
        } while (item != firstItem);
    }

    public void setExcludedFlags(boolean value) {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return;
        }

        do {
            item.excluded = value;
            item = item.next;
        } while (item != firstItem);
    }

    private void updateReachableFlags(AVM2InstructionItem jump, AVM2InstructionItem jumpTarget) {
        if (firstItem == null) {
            return;
        }

        clearReachableFlags();

        firstItem.reachable = 1;
        AVM2InstructionItem firstItem2 = firstItem;
        boolean modified = true;
        while (modified) {
            modified = false;
            AVM2InstructionItem item = firstItem2;
            do {
                AVM2InstructionItem next = item.next;
                //AVM2InstructionItem alternativeNext = null;
                AVM2Instruction action = item.ins;
                if (item.reachable == 1) {
                    item.reachable = 2;
                    modified = true;

                    if (item == firstItem2) {
                        firstItem2 = next;
                    }

                    if (item == jump) {
                        if (jumpTarget.reachable == 0) {
                            jumpTarget.reachable = 1;
                            //alternativeNext = jumpTarget;
                        }
                    } else {

                        if (!action.isExit() && !(action.definition instanceof JumpIns)) {
                            if (next.reachable == 0) {
                                next.reachable = 1;
                            }
                        }

                        if (action instanceof GraphSourceItemContainer) {
                            for (AVM2InstructionItem lastActionItem : item.getContainerLastInstructions()) {
                                if (lastActionItem != null && lastActionItem.next != null && lastActionItem.next.reachable == 0) {
                                    lastActionItem.next.reachable = 1;
                                    //alternativeNext = lastActionItem.next;
                                }
                            }
                        }

                        AVM2InstructionItem target = item.getJumpTarget();
                        if (target != null) {
                            if (target.reachable == 0) {
                                target.reachable = 1;
                                //alternativeNext = target;
                            }
                        }
                    }
                }

                //item = alternativeNext == null || next.reachable == 1 ? next : alternativeNext;
                item = next;
            } while (item != firstItem);
        }
    }

    public void updateActions(MethodBody body) {
        AVM2Code result = new AVM2Code(size);
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            body.setCode(result);
            body.exceptions = new ABCException[0];
            return;
        }

        List<AVM2Instruction> resultList = result.code;
        do {
            resultList.add(item.ins);
            item = item.next;
        } while (item != firstItem);

        updateActionAddressesAndLengths();
        updateJumps();
        updateContainerSizes();
    }

    public AVM2InstructionItem first() {
        return firstItem;
    }

    public AVM2InstructionItem last() {
        return firstItem == null ? null : firstItem.prev;
    }

    public void toMethodBody(MethodBody body) {
        updateActions(body);
    }

    @Override
    public int size() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public boolean contains(Object o) {
        if (o instanceof AVM2InstructionItem) {
            return actionItemSet.contains(o);
        } else if (o instanceof AVM2Instruction) {
            return actionItemMap.containsKey((AVM2Instruction) o);
        }

        return false;
    }

    @Override
    public FastAVM2ListIterator iterator() {
        return new FastAVM2ListIterator(this);
    }

    @Override
    public Object[] toArray() {
        Object[] result = new Object[size];

        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return result;
        }

        int i = 0;
        do {
            result[i] = item.ins;
            item = item.next;
            i++;
        } while (item != firstItem);
        return null;
    }

    @Override
    @SuppressWarnings("unchecked")
    public <T> T[] toArray(T[] a) {
        if (a.length != size) {
            a = (T[]) new AVM2InstructionItem[size];
        }

        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return a;
        }

        int i = 0;
        do {
            a[i] = (T) item;
            item = item.next;
            i++;
        } while (item != firstItem);
        return null;
    }

    @Override
    public boolean add(AVM2InstructionItem e) {
        insertItemAfter(null, e);
        return true;
    }

    @Override
    public boolean remove(Object o) {
        AVM2InstructionItem item = null;
        if (o instanceof AVM2InstructionItem) {
            item = (AVM2InstructionItem) o;
        } else if (o instanceof AVM2Instruction) {
            item = actionItemMap.get((AVM2Instruction) o);
        }

        if (item == null) {
            return false;
        }

        removeItem(item);
        return true;
    }

    @Override
    public boolean containsAll(Collection<?> c) {
        for (Object c1 : c) {
            if (!contains(c1)) {
                return false;
            }
        }

        return true;
    }

    @Override
    public boolean addAll(Collection<? extends AVM2InstructionItem> c) {
        for (AVM2InstructionItem c1 : c) {
            insertItemAfter(null, c1);
        }

        return true;
    }

    @Override
    public boolean removeAll(Collection<?> c) {
        boolean result = false;
        for (Object c1 : c) {
            result |= remove(c1);
        }

        return result;
    }

    @Override
    public boolean retainAll(Collection<?> c) {
        AVM2InstructionItem item = firstItem;
        if (item == null) {
            return false;
        }

        boolean modified = false;
        do {
            if (!c.contains(item)) {
                item = removeItem(item);
                modified = true;
                continue;
            }

            item = item.next;
        } while (item != firstItem);
        return modified;
    }

    @Override
    public void clear() {
        firstItem = null;
        size = 0;
    }
}
