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

import java.util.Collection;
import java.util.ListIterator;

import org.apache.royale.compiler.common.ISourceLocation;
import org.apache.royale.compiler.common.PrefixMap;
import org.apache.royale.compiler.common.SourceLocation;
import org.apache.royale.compiler.common.XMLName;
import org.apache.royale.compiler.filespecs.IFileSpecification;
import org.apache.royale.compiler.internal.parsing.ISourceFragment;
import org.apache.royale.compiler.internal.parsing.SourceFragment;
import org.apache.royale.compiler.internal.parsing.mxml.MXMLToken;
import org.apache.royale.compiler.mxml.IMXMLTagAttributeData;
import org.apache.royale.compiler.mxml.IMXMLTagData;
import org.apache.royale.compiler.parsing.MXMLTokenTypes;
import org.apache.royale.compiler.problems.ICompilerProblem;
import org.apache.royale.compiler.problems.MXMLUnclosedTagProblem;
import org.apache.royale.compiler.problems.SyntaxProblem;

/**
 * Represents a tag attribute in MXML.
 */
public class MXMLTagAttributeData extends SourceLocation implements IMXMLTagAttributeData
{
    /**
     * Constructor.
     * <p>
     * Each attribute consumes three tokens:
     * {@code TOKEN_NAME}, {@code TOKEN_EQUALS}, and {@code TOKEN_STRING}.
     */
    MXMLTagAttributeData(MXMLToken nameToken, ListIterator<MXMLToken> tokenIterator,
                         MXMLDialect mxmlDialect, IFileSpecification spec,
                         Collection<ICompilerProblem> problems)
    {
        setStart(nameToken.getStart());
        setLine(nameToken.getLine());
        setColumn(nameToken.getColumn());
        setEndLine(nameToken.getEndLine());
        setEndColumn(nameToken.getEndColumn());
        setEnd(nameToken.getEnd());

        // Deal with name if it is of the form name.state
        MXMLStateSplitter splitState = new MXMLStateSplitter(nameToken, mxmlDialect, problems, spec);
        attributeName = splitState.getBaseName();
        if (splitState.getStateName() != null)
        {
            stateName = splitState.getStateName();
            stateStart = nameToken.getStart() + splitState.getStateNameOffset();
        }

        MXMLToken token = null;

        // Look for "=" token.
        if (tokenIterator.hasNext())
        {
            token = tokenIterator.next();

            if (token.getType() != MXMLTokenTypes.TOKEN_EQUALS)
            {
                if (token.getSourcePath() == null)
                    token.setSourcePath(spec.getPath());
                problems.add(new SyntaxProblem(token));
                // Restore the token position for error recovery.
                tokenIterator.previous();
                return;
            }

            valueStart = token.getEnd(); // set the value's start to right after the equals until we have a value
            valueLine = token.getLine();
            valueColumn = token.getColumn();
        }

        Boolean firstToken = true;
        
        // Look for value token.
        while (tokenIterator.hasNext())
        {
            token = tokenIterator.next();
            if (token.getType() == MXMLTokenTypes.TOKEN_STRING)
            {
                valueIncludingDelimiters = token.getText();
            }
            else
            {
                if (firstToken)
                {
                    if (token.getSourcePath() == null)
                        token.setSourcePath(spec.getPath());
                    problems.add(new SyntaxProblem(token));
                }
                else if (!MXMLToken.isTagEnd(token.getType()) && token.getType() != MXMLTokenTypes.TOKEN_NAME)
                {
                    if (token.getSourcePath() == null)
                        token.setSourcePath(spec.getPath());
                    problems.add(new MXMLUnclosedTagProblem(token));
                }
                // Restore the token position for error recovery.
                tokenIterator.previous();
                return;
            }
            firstToken = false;
        }
    }

    /**
     * The MXML tag that contains this attribute.
     */
    protected IMXMLTagData parent;

    /**
     * The URI specified by this attribute's prefix.
     */
    protected String uri;

    /**
     * The name of this attribute.
     */
    protected String attributeName;

    /**
     * The name of this state, if it exists.
     */
    protected String stateName;

    /**
     * The offset at which the optional state starts.
     */
    protected int stateStart;
    
    /**
     * The attribute value, including any delimiters.
     */
    protected String valueIncludingDelimiters;

    /**
     * The offset at which the attribute value starts.
     */
    protected int valueStart;

    /**
     * The line on which the attribute value starts.
     */
    protected int valueLine;

    /**
     * The column at which the attribute value starts.
     */
    protected int valueColumn;

    //
    // Object overrides.
    //

    // For debugging only. This format is nice in the Eclipse debugger.
    @Override
    public String toString()
    {
        StringBuilder sb = new StringBuilder();
        buildAttributeString(false);
        return sb.toString();
    }

    //
    // IMXMLTagAttributeData implementations
    //

    @Override
    public MXMLDialect getMXMLDialect()
    {
        return getParent().getParent().getMXMLDialect();
    }

    @Override
    public IMXMLTagData getParent()
    {
        return parent;
    }
    
    @Override
    public String getName()
    {
        return attributeName;
    }

    @Override
    public String getPrefix()
    {
        String name = getName();
        int i = name.indexOf(':');
        return i != -1 ? name.substring(0, i) : null;
    }

    @Override
    public String getURI()
    {
        if (uri == null)
        {
            //walk up our chain to find the correct uri for our namespace.  first one wins
            String prefix = getPrefix();
            if (prefix == null)
                return null;

            IMXMLTagData lookingAt = parent;

            // For attributes with prefix, parent's parent can be null if
            // parent is the root tag 
            while (lookingAt != null && lookingAt.getParent() != null)
            {
                PrefixMap depth = lookingAt.getParent().getPrefixMapForData(lookingAt);
                if (depth != null && depth.containsPrefix(prefix))
                {
                    uri = depth.getNamespaceForPrefix(prefix);
                    break;
                }

                lookingAt = lookingAt.getParentTag();
            }
        }

        return uri;
    }

    @Override
    public String getShortName()
    {
        String name = getName();
        int i = name.indexOf(':');
        return i != -1 ? name.substring(i + 1) : name;
    }

    @Override
    public XMLName getXMLName()
    {
        return new XMLName(getURI(), getShortName());
    }

    @Override
    public String getStateName()
    {
        return stateName != null ? stateName : "";
    }

    @Override
    public boolean isSpecialAttribute(String name)
    {
        String languageURI = getMXMLDialect().getLanguageNamespace();

        return getName().equals(name) && (getPrefix() == null || getURI() == languageURI);
    }

    @Override
    public boolean hasValue()
    {
        return getRawValue() != null;
    }

    @Override
    public String getRawValue()
    {
        String value = getValueWithQuotes();

        if (value != null && value.length() > 0)
        {
            // length can be one in case of invalid data and then the substring() call fails
            // so, handle it here
            if (value.charAt(0) == value.charAt(value.length() - 1) && value.length() != 1)
                value = value.substring(1, value.length() - 1);
            else
                value = value.substring(1);
        }

        return value;
    }

    @Override
    public ISourceFragment[] getValueFragments(Collection<ICompilerProblem> problems)
    {
        String value = getRawValue();
        if (value == null)
            return new ISourceFragment[0];
        
        ISourceLocation location = getValueLocation();
        MXMLDialect mxmlDialect = getMXMLDialect();

        String s = EntityProcessor.parseAsString(value, location, mxmlDialect, problems);
        ISourceFragment[] result = new ISourceFragment[1];
        result[0] = new SourceFragment(s, location);
        return result;
    }

    @Override
    public int getValueStart()
    {
        return hasValue() ? valueStart + 1 : 0;
    }

    @Override
    public int getValueEnd()
    {
        if (hasValue())
              return getValueStart() + getRawValue().length();

        // If there is no valid "end", then we must return -1. Callers depend on this.
        // See MXMLTagData.findArttributeContainingOffset for an example
        return -1;
    }

    @Override
    public int getValueLine()
    {
        return hasValue() ? valueLine : 0;
    }

    @Override
    public int getValueColumn()
    {
        return hasValue() ? valueColumn + 1 : 0;
    }

    @Override
    public SourceLocation getValueLocation()
    {
        return new SourceLocation(getSourcePath(), getValueStart(), getValueEnd(), getValueLine(), getValueColumn());
    }

    //
    // Other methods
    //

    public IFileSpecification getSource()
    {
        return getParent().getSource();
    }

    /**
     * Sets this attribute's tag.
     * 
     * @param parent MXML tag containing this attribute
     */
    public void setParent(IMXMLTagData parent)
    {
        this.parent = parent;
        setSourcePath(parent.getSourcePath());
    }

    /**
     * Adjust all associated offsets by the adjustment amount
     * 
     * @param offsetAdjustment amount to add to offsets
     */
    public void adjustOffsets(int offsetAdjustment)
    {
        if (attributeName != null)
        {
            setStart(getAbsoluteStart() + offsetAdjustment);
            setEnd(getAbsoluteEnd() + offsetAdjustment);
        }

        if (hasValue())
            valueStart += offsetAdjustment;

        if (stateName != null)
            stateStart += offsetAdjustment;
    }

    /**
     * Returns the {@link PrefixMap} that represents all prefix->namespace
     * mappings are in play on this tag. For example, if a parent tag defines
     * <code>xmlns:m="royale"</code> and this tag defines
     * <code>xmlns:m="eagle"</code> then in this prefix map, m will equal
     * "eagle"
     * 
     * @return a {@link PrefixMap} or null
     */
    public PrefixMap getCompositePrefixMap()
    {
        return parent.getCompositePrefixMap();
    }

    void invalidateURI()
    {
        uri = null;
    }

    /**
     * Gets the starting offset for this attribute's name.
     */
    public int getNameStart()
    {
        return getAbsoluteStart();
    }

    /**
     * Gets the ending offset for this attribute's name.
     */
    public int getNameEnd()
    {
        return getAbsoluteStart() + attributeName.length();
    }

    /**
     * Gets the line number for this attribute's name.
     * 
     * @return end offset
     */
    public final int getNameLine()
    {
        return getLine();
    }

    /**
     * Get the column number for this attribute's name.
     * 
     * @return end offset
     */
    public final int getNameColumn()
    {
        return getColumn();
    }

    /**
     * Does the offset fall inside the bounds of the attribute name?
     * 
     * @param offset test offset
     * @return true if the offset falls within the attribute name
     */
    public boolean isInsideName(int offset)
    {
        if (attributeName != null)
            return MXMLData.contains(getNameStart(), getNameEnd(), offset);

        return false;
    }

    /**
     * Checks whether this attribute is associated with a state.
     * 
     * @return True if a state association exists.
     */
    public boolean hasState()
    {
        return stateName != null;
    }

    /**
     * Get this attribute's state start offset if a state token is present other
     * wise zero.
     * 
     * @return state start offset or zero
     */
    public int getStateStart()
    {
        return stateName != null ? stateStart : 0;
    }

    /**
     * Get this attribute's state tokens end offset if a state token is present
     * other wise zero.
     * 
     * @return state start offset or zero
     */
    public int getStateEnd()
    {
        return stateName != null ? stateStart + stateName.length() : 0;
    }
    
    public boolean isInsideStateName(int offset)
    {
        if (stateName != null)
            return MXMLData.contains(getStateStart(), getStateEnd(), offset);

        return false;
    }

    /**
     * Gets the attribute value as a String (with quotes).
     * 
     * @return attribute value (with quotes)
     */
    public String getValueWithQuotes()
    {
        return valueIncludingDelimiters;
    }

    /**
     * Does this value have a closing quote character?
     * 
     * @return true if this value has a closing quote character
     */
    protected boolean valueIsWellFormed()
    {
        // If there is a value, it came from a string token.  We know (from the
        // RawTagTokenizer) that this means it starts with a quote character.  If
        // it ends with the same quote character, it's well formed.
        if (hasValue())
        {
            char firstChar = valueIncludingDelimiters.charAt(0);
            char lastChar = valueIncludingDelimiters.charAt(valueIncludingDelimiters.length() - 1);
            return (firstChar == '"' || firstChar == '\'') && firstChar == lastChar;
        }

        return false;
    }

    /**
     * Does the offset fall inside the bounds of the attribute value?
     * 
     * @param offset test offset
     * @return true if the offset falls within the attribute value
     */
    public boolean isInsideValue(int offset)
    {
        if (hasValue())
            return MXMLData.contains(getValueStart() - 1, getValueEnd(), offset);

        return false;
    }
    
    /**
     * For unit tests only.
     * 
     * @return name value and offsets in string form
     */
    public String buildAttributeString(boolean skipSrcPath)
    {
        StringBuilder sb = new StringBuilder();
        sb.append(getName());
        sb.append('=');
        sb.append('"');
        sb.append(getRawValue());
        sb.append('"');

        sb.append(' ');

        // Display line, column, start, and end as "17:5 160-188".
        if (skipSrcPath)
            sb.append(getOffsetsString());
        else
            sb.append(super.toString());
        
        return sb.toString();
    }

    /**
     * Verifies that this attribute has its source location information set.
     * <p>
     * This is used only in asserts.
     */
    public boolean verify()
    {
        // Verify the source location.
        assert getSourcePath() != null : "Attribute has null source path: " + toString();
        assert getStart() != UNKNOWN : "Attribute has unknown start: " + toString();
        assert getEnd() != UNKNOWN : "Attribute has unknown end: " + toString();
        assert getLine() != UNKNOWN : "Attribute has unknown line: " + toString();
        assert getColumn() != UNKNOWN : "Attribute has unknown column: " + toString();

        return true;
    }
}
