/*
 * Bytecode Analysis Framework
 * Copyright (C) 2004,2008 University of Maryland
 *
 * 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 2.1 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

package edu.umd.cs.findbugs.ba;

import java.util.Iterator;

import org.apache.bcel.generic.InstructionHandle;

/**
 * A Path is a sequence of basic blocks.
 *
 * @author David Hovemeyer
 */
public class Path {
    private static final int DEFAULT_CAPACITY = 8;

    private static final int INVALID_HASH_CODE = -1;

    private int[] blockIdList;

    private int length;

    private int cachedHashCode;

    /**
     * Constructor. Creates an empty Path.
     */
    public Path() {
        this.blockIdList = new int[DEFAULT_CAPACITY];
        this.length = 0;
        invalidate();
    }

    /**
     * Append given BasicBlock id to the path.
     *
     * @param id
     *            a BasicBlock id (label)
     */
    public void append(int id) {
        grow(length);
        blockIdList[length] = id;
        ++length;
        invalidate();
    }

    /**
     * Determine whether or not the id of the given BasicBlock appears anywhere
     * in the path.
     *
     * @param blockId
     *            the id (label) of a BasicBlock
     * @return true if the BasicBlock's id appears in the path, false if not
     */
    public boolean hasComponent(int blockId) {
        for (int i = 0; i < length; i++) {
            if (blockIdList[i] == blockId) {
                return true;
            }
        }
        return false;
    }

    /**
     * Get the BasicBlock id at the given index in the path.
     *
     * @param index
     *            an index in the Path (0 is the first component)
     * @return the id of the BasicBlock at the given index
     */
    public int getBlockIdAt(int index) {
        assert index < length;
        return blockIdList[index];
    }

    /**
     * Get the number of components (BasicBlock ids) in the Path.
     *
     * @return number of components in the Path
     */
    public int getLength() {
        return length;
    }

    /**
     * Return an exact copy of this Path.
     *
     * @return an exact copy of this Path
     */
    public Path duplicate() {
        Path dup = new Path();
        dup.copyFrom(this);
        return dup;
    }

    /**
     * Make this Path identical to the given one.
     *
     * @param other
     *            a Path to which this object should be made identical
     */
    public void copyFrom(Path other) {
        grow(other.length - 1);
        System.arraycopy(other.blockIdList, 0, this.blockIdList, 0, other.length);
        this.length = other.length;
        this.cachedHashCode = other.cachedHashCode;
    }

    /**
     * Accept a PathVisitor.
     *
     * @param cfg
     *            the control flow graph
     * @param visitor
     *            a PathVisitor
     */
    public void acceptVisitor(CFG cfg, PathVisitor visitor) {
        if (getLength() > 0) {
            BasicBlock startBlock = cfg.lookupBlockByLabel(getBlockIdAt(0));
            acceptVisitorStartingFromLocation(cfg, visitor, startBlock, startBlock.getFirstInstruction());
        }
    }

    /**
     * Accept a PathVisitor, starting from a given BasicBlock and
     * InstructionHandle.
     *
     * @param cfg
     *            the control flow graph
     * @param visitor
     *            a PathVisitor
     * @param startBlock
     *            BasicBlock where traversal should start
     * @param startHandle
     *            InstructionHandle within the start block where traversal
     *            should start
     */
    public void acceptVisitorStartingFromLocation(CFG cfg, PathVisitor visitor, BasicBlock startBlock,
            InstructionHandle startHandle) {
        // Find the start block in the path
        int index;
        for (index = 0; index < getLength(); index++) {
            if (getBlockIdAt(index) == startBlock.getLabel()) {
                break;
            }
        }

        assert index < getLength();

        Iterator<InstructionHandle> i = startBlock.instructionIterator();

        // Position iterator at start instruction handle
        if (startHandle != startBlock.getFirstInstruction()) {
            while (i.hasNext()) {
                InstructionHandle handle = i.next();
                if (handle.getNext() == startHandle) {
                    break;
                }
            }
        }

        BasicBlock basicBlock = startBlock;
        while (true) {
            // visit block
            visitor.visitBasicBlock(basicBlock);

            // visit instructions in block
            while (i.hasNext()) {
                visitor.visitInstructionHandle(i.next());
            }

            // end of path?
            index++;
            if (index >= getLength()) {
                break;
            }

            // visit edge
            BasicBlock next = cfg.lookupBlockByLabel(getBlockIdAt(index));
            Edge edge = cfg.lookupEdge(basicBlock, next);
            assert edge != null;
            visitor.visitEdge(edge);

            // continue to next block
            basicBlock = next;
            i = basicBlock.instructionIterator();
        }
    }

    /**
     * Determine whether or not given Path is a prefix of this one.
     *
     * @param path
     *            another Path
     * @return true if this Path is a prefix of the other Path, false otherwise
     */
    public boolean isPrefixOf(Path path) {
        if (this.getLength() > path.getLength()) {
            return false;
        }
        for (int i = 0; i < getLength(); i++) {
            if (this.getBlockIdAt(i) != path.getBlockIdAt(i)) {
                return false;
            }
        }
        return true;
    }

    private void invalidate() {
        this.cachedHashCode = INVALID_HASH_CODE;
    }

    @Override
    public int hashCode() {
        if (cachedHashCode == INVALID_HASH_CODE) {
            int value = 0;
            for (int i = 0; i < this.length; ++i) {
                value += (i * 1009 * blockIdList[i]);
            }
            cachedHashCode = value;
        }
        return cachedHashCode;
    }

    @Override
    public boolean equals(Object o) {
        if (o == null || o.getClass() != this.getClass()) {
            return false;
        }
        Path other = (Path) o;
        if (this.length != other.length) {
            return false;
        }
        for (int i = 0; i < this.length; ++i) {
            if (this.blockIdList[i] != other.blockIdList[i]) {
                return false;
            }
        }
        return true;
    }

    private static final String SYMBOLS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*()";

    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        for (int i = 0; i < length; ++i) {
            int block = blockIdList[i];
            if (block < SYMBOLS.length()) {
                buf.append(SYMBOLS.charAt(block));
            } else {
                buf.append("'" + block + "'");
            }
        }
        return buf.toString();
    }

    private void grow(int index) {
        if (index >= blockIdList.length) {
            int newLen = blockIdList.length;
            do {
                newLen *= 2;
            } while (index >= newLen);

            int[] arr = new int[newLen];
            System.arraycopy(this.blockIdList, 0, arr, 0, length);
            this.blockIdList = arr;
        }
    }
}

