/*
 *
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You under the Apache License, Version 2.0
 *  (the "License"); you may not use this file except in compliance with
 *  the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 *
 */

package org.apache.royale.compiler.internal.parsing.as;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;
import com.google.common.collect.Ordering;

/**
 * Tokens generated from {@link StreamingASTokenizer} has been altered so that
 * their start and end offsets are absolute offsets.
 * <P>
 * This class can translate an "absolute" offset to a tuple of filename and
 * "local offset" in the file. Each ASFileScope has an
 * {@code OffsetLookup} object.
 */
public class OffsetLookup
{
    /**
     * Comparator to sort {@code OffetCue} by absolute offset in ascending
     * order.
     */
    private static final Comparator<OffsetCue> OFFSET_CUE_COMPARATOR =
            new Comparator<OffsetCue>()
    {
        @Override
        public int compare(OffsetCue o1, OffsetCue o2)
        {
            return o1.absolute - o2.absolute;
        }
    };

    /**
     * This is used to sort the immutable list and do binary-search.
     */
    private static final Ordering<OffsetCue> ORDER_BY_ABSOLUTE =
            Ordering.from(OFFSET_CUE_COMPARATOR);

    /**
     * Create a wrapper {@code OffsetCue} object in for binary-searching the
     * {@link #offsetCueList}
     * 
     * @param absolute absolute offset
     * @return The {@code OffsetCue} that applies to the given absolute offset.
     */
    private static OffsetCue createSearchKey(int absolute)
    {
        return new OffsetCue("", absolute, 0);
    }

    public OffsetLookup(final ImmutableList<OffsetCue> offsetCueList)
    {
        assert offsetCueList != null : "Offset cue list can't be null.";
        assert ORDER_BY_ABSOLUTE.isOrdered(offsetCueList) : "Offset cue list should be sorted by absolute offset.";
        // For now the creation of offsetCueList guarantees the ordering of the
        // list, so we needn't do a sorted immutable copy.
        this.offsetCueList = offsetCueList;
    }

    private final ImmutableList<OffsetCue> offsetCueList;

    /**
     * Find the nearest {@link OffsetCue} before the given absolute offset
     * 
     * @param absoluteOffset absolute offset
     * @return The {@code OffsetCue} that applies to the given absolute offset.
     */
    private OffsetCue findOffsetCue(int absoluteOffset)
    {
        if (offsetCueList.isEmpty() || absoluteOffset < 0)
            return null;

        final OffsetCue key = createSearchKey(absoluteOffset);
        final int index = ORDER_BY_ABSOLUTE.binarySearch(offsetCueList, key);

        // The return value of "binarySearch" is either the index of the found
        // item, or (-(insertion point) - 1).
        if (index >= 0)
        {
            return offsetCueList.get(index);
        }
        else
        {
            final int insertionPoint = -(index + 1);
            return offsetCueList.get(insertionPoint - 1);
        }

    }

    /**
     * Get the name of the file that contains the given absolute offset.
     * 
     * @param absoluteOffset absolute offset
     * @return Name of the file that contains the given absolute offset.
     */
    public String getFilename(final int absoluteOffset)
    {
        final OffsetCue result = findOffsetCue(absoluteOffset);
        if (result == null)
            return null;
        else
            return result.filename;
    }

    /**
     * Translate absolute offset to local offset.
     * 
     * @param absoluteOffset absolute offset
     * @return Local offset.
     */
    public int getLocalOffset(final int absoluteOffset)
    {
        final OffsetCue result = findOffsetCue(absoluteOffset);
        if (result == null)
            return absoluteOffset;
        else
            return absoluteOffset - result.adjustment;
    }

    /**
     * Convert a local offset to an absolute offset. If a file is included more
     * than once, there would be more than one absolute offset corresponding to
     * a given local offset within that included file.
     * 
     * @param filename Normalized path of the file the local offset is in.
     * @param localOffset Local offset.
     * @return Absolute offsets corresponding to the given local offset.
     */
    public int[] getAbsoluteOffset(final String filename, final int localOffset)
    {
        if (offsetCueList.isEmpty())
        {
            return new int[] {localOffset};
        }

        assert filename != null : "Filename can't be null.";
        if (localOffset < 0)
            return new int[] {localOffset};

        // OffsetCue objects in the given file.
        final Iterable<OffsetCue> fileOffsetCueList =
                Iterables.filter(offsetCueList, new Predicate<OffsetCue>()
                {
                    @Override
                    public boolean apply(OffsetCue cue)
                    {
                        return cue.filename.equals(filename);
                    }
                });

        // Find a list of OffsetCues before the local offset.
        final List<OffsetCue> candidateCues = new ArrayList<OffsetCue>(1);
        OffsetCue candidate = null;
        for (final OffsetCue cue : fileOffsetCueList)
        {
            if (cue.local <= localOffset)
            {
                candidate = cue;
            }
            else if (candidate != null)
            {
                candidateCues.add(candidate);
                candidate = null;
            }
        }

        // There is no OffsetCue to end and included file. 
        // The last one needs to be manually added.
        if (candidate != null)
            candidateCues.add(candidate);

        if (candidateCues.isEmpty())
        {
            throw new IllegalArgumentException(
                    "Local offset '" + localOffset + "' is not in file " + filename);
        }

        // Collect the absolute offsets.
        final int matchSize = candidateCues.size();
        final int[] absoluteOffsets = new int[matchSize];
        for (int i = 0; i < matchSize; i++)
        {
            final OffsetCue cue = candidateCues.get(i);
            absoluteOffsets[i] = cue.absolute + (localOffset - cue.local);
        }
        return absoluteOffsets;
    }

    @Override
    public String toString()
    {
        return Joiner.on("\n").join(offsetCueList);
    }

    /**
     * Determines if there are any includes.
     * 
     * @return true if there are any included files, false otherwise.
     */
    public boolean hasIncludes()
    {
        // If there are no Cue's then there are no includes.
        // If there is only one Cue, then there are no includes.
        return offsetCueList.size() > 1;
    }
}
