/*
 * 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.netbeans.modules.html.parser;

import java.util.*;
import org.netbeans.modules.html.editor.lib.api.ProblemDescription;
import org.netbeans.modules.html.editor.lib.api.elements.*;
import org.netbeans.modules.web.common.api.LexerUtils;
import org.openide.util.CharSequences;

/**
 *
 * @author marekfukala
 */
public class ElementsFactory {

    private CharSequence source;

    ElementsFactory(CharSequence source) {
        this.source = source;
    }

    Text createText(int startOffset, int endOffset) {
        return new Text(startOffset, endOffset, source);
    }
    
    ModifiableCloseTag createCloseTag(int startOffset, int endOffset, byte nameLen) {
        return new CommonCloseTag(source, startOffset, endOffset, nameLen);
    }

    ModifiableOpenTag createOpenTag(int startOffset, int endOffset, byte nameLen) {
        return new CommonOpenTag(source, startOffset, endOffset, nameLen);
    }

    ModifiableOpenTag createVirtualOpenTag(CharSequence name) {
        return new VirtualOpenTag(name);
    }

    ModifiableOpenTag createEmptyOpenTag(int startOffset, int endOffset, byte nameLen) {
        return new EmptyOpenTag(source, startOffset, endOffset, nameLen);
    }

    Root createRoot() {
        return new Root(source, null);
    }

    CommonAttribute createAttribute(int nameOffset, int valueOffset, short nameLen, int valueLen) {
        return new CommonAttribute(source, nameOffset, valueOffset, nameLen, valueLen);
    }

    CommonAttribute createAttribute(int nameOffset, short nameLen) {
        return new CommonAttribute(source, nameOffset, nameLen);
    }

    static interface ModifiableElement extends Element {

        public void detachFromParent();
        
        public void setEndOffset(int endOffset);
        
        public void setParent(Node parent);
    }
    
    static interface ModifiableCloseTag extends ModifiableElement, CloseTag {
        
        public void setMatchingOpenTag(OpenTag openTag);
        
    }

    static interface ModifiableOpenTag extends ModifiableElement, OpenTag {

        public void addChild(Element element);
        
        public void insertChildBefore(Element toInsert, Element element);

        public void addChildren(Collection<Element> element);

        public void removeChildren(Collection<Element> children);

        public void removeChild(Element element);

        public void setSemanticEndOffset(int endOffset);
        
        public void setMatchingCloseTag(CloseTag closeTag);

        public void setAttribute(Attribute attribute);
    }

    static class Text implements ModifiableElement {

        private int from, to;
        private CharSequence source;
        private Node parent;

        public Text(int from, int to, CharSequence source) {
            this.from = from;
            this.to = to;
            this.source = source;
        }
        
        @Override
        public int from() {
            return from;
        }

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

        @Override
        public ElementType type() {
            return ElementType.TEXT;
        }

        @Override
        public CharSequence image() {
            return source.subSequence(from, to);
        }

        @Override
        public CharSequence id() {
            return null;
        }

        @Override
        public Collection<ProblemDescription> problems() {
            return Collections.emptyList();
        }

        @Override
        public Node parent() {
            return parent;
        }

        @Override
        public void detachFromParent() {
        }

        @Override
        public void setEndOffset(int endOffset) {
            to = endOffset;
        }

        @Override
        public void setParent(Node parent) {
            this.parent = parent;
        }

        @Override
        public String toString() {
            return new StringBuilder()
                    .append(type().name())
                    .append(';')
                    .append(' ')
                    .append(from())
                    .append("-")
                    .append(to())
                    .append(' ')
                    .append('"')
                    .append(escapeEOLs(image()))
                    .append('"')
                    .toString();
        }
        
        private String escapeEOLs(CharSequence text) {
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i < text.length(); i++) {
                char c = text.charAt(i);
                if(c == '\n') {
                    sb.append("\\n");
                } else {
                    sb.append(c);
                }
            }
            return sb.toString();
        }
        
    }
    
    abstract static class TagElement implements Named, ModifiableElement {

        //32-64
        protected CharSequence source;
        //32
        //element start
        private int startOffset;
        //16
        //element end - element start. Should fit into 2^16
        private short endOffsetToStartOffsetDiff;
        //8
        //lenght of the name of the element
        private byte nameLen;
        //32-64
        private Node parent;

        //sum (32bit JVM): 120bits=15bytes
        public TagElement(CharSequence source, int startOffset, int endOffset, byte nameLen) {
            assert nameLen >= 0;
            
            this.source = source;
            this.startOffset = startOffset;
            setEndOffset(endOffset);
            setNameLen(nameLen);
        }

        @Override
        public void setEndOffset(int endOffset) {
            this.endOffsetToStartOffsetDiff = (short) (endOffset - startOffset);
        }

        protected void setNameLen(byte nameLen) {
            assert nameLen >= 0;
            this.nameLen = nameLen;
        }

        //1 for open tag (< between startOffset and the name), 2 for end tag (</ ...)
        protected abstract int nameOffsetToStartOffsetDiff();

        private int nameOffset() {
            return startOffset + nameOffsetToStartOffsetDiff();
        }

        @Override
        public CharSequence name() {
            return source.subSequence(nameOffset(), nameOffset() + nameLen);
        }

        @Override
        public CharSequence namespacePrefix() {
            int colonIndex = CharSequences.indexOf(name(), ":");
            return colonIndex == -1 ? null : name().subSequence(0, colonIndex);
        }

        @Override
        public CharSequence unqualifiedName() {
            int colonIndex = CharSequences.indexOf(name(), ":");
            return colonIndex == -1 ? name() : name().subSequence(colonIndex + 1, name().length());
        }

        @Override
        public int from() {
            return startOffset;
        }

        @Override
        public int to() {
            return startOffset + endOffsetToStartOffsetDiff;
        }

        @Override
        public CharSequence image() {
            return source.subSequence(from(), to());
        }

        @Override
        public CharSequence id() {
            return name();
        }

        @Override
        public Collection<ProblemDescription> problems() {
            return Collections.emptyList();
        }

        @Override
        public void setParent(Node parent) {
            this.parent = parent;
        }

        @Override
        public Node parent() {
            return parent;
        }

        @Override
        public String toString() {
            return new StringBuilder().append(name()).append("(").append(type().name()).append(")").append("; ").append(from()).append("-").append(to()).toString();
        }

        @Override
        public void detachFromParent() {
//            ModifiableOpenTag mot = (ModifiableOpenTag) parent();
//            mot.removeChild(this);
            setParent(null);
        }
    }

    static class EmptyOpenTag extends TagElement implements OpenTag, ModifiableOpenTag {

        private Collection<Attribute> attrs;

        public EmptyOpenTag(CharSequence source, int startOffset, int endOffset, byte nameLen) {
            super(source, startOffset, endOffset, nameLen);
        }

        void addAttribute(Attribute attr) {
            if(attrs == null) {
                attrs = new ArrayList<Attribute>();
            }
            attrs.add(attr);
        }

        void addAttributes(Collection<Attribute> attributes) {
            if(attrs == null) {
                attrs = new ArrayList<Attribute>();
            }
            attrs.addAll(attributes);
        }

        @Override
        public Collection<Attribute> attributes() {
            return attrs == null ? Collections.<Attribute>emptyList() : attrs;
        }

        @Override
        public Collection<Attribute> attributes(AttributeFilter filter) {
            Collection<Attribute> filtered = new ArrayList<Attribute>(1);
            for (Attribute a : attributes()) {
                if (filter.accepts(a)) {
                    filtered.add(a);
                }
            }
            return filtered;
        }

        @Override
        public Attribute getAttribute(String name) {
            //typically very low number of attrs so the linear search doesn't hurt
            for (Attribute a : attributes()) {
                if (LexerUtils.equals(name, a.name(), true, false)) {
                    return a;
                }
            }
            return null;
        }

        @Override
        public boolean isEmpty() {
            return true;
        }

        @Override
        public CloseTag matchingCloseTag() {
            return null;
        }

        @Override
        public int semanticEnd() {
            return to();
        }

        @Override
        public Collection<Element> children() {
            return Collections.emptyList();
        }

        @Override
        public Collection<Element> children(ElementType type) {
            return Collections.emptyList();
        }

        @Override
        public Collection<Element> children(ElementFilter filter) {
            return Collections.emptyList();
        }

        @Override
        public <T extends Element> Collection<T> children(Class<T> type) {
            return Collections.emptyList();
        }

        @Override
        public ElementType type() {
            return ElementType.OPEN_TAG;
        }

        @Override
        protected int nameOffsetToStartOffsetDiff() {
            return 1; //"<".length();
        }

        @Override
        public void addChild(Element element) {
            //no-op
//            throw new IllegalStateException();
        }

        @Override
        public void removeChild(Element element) {
            //no-op
//            throw new IllegalStateException();
        }

        @Override
        public void setSemanticEndOffset(int endOffset) {
            //no-op
        }

        @Override
        public void setAttribute(Attribute attribute) {
            addAttribute(attribute);
        }

        @Override
        public void removeChildren(Collection<Element> children) {
            //no-op
//            throw new IllegalStateException();
        }

        @Override
        public void addChildren(Collection<Element> element) {
            //no-op
//            throw new IllegalStateException();
        }

        @Override
        public void insertChildBefore(Element toInsert, Element element) {
            //no-op
//            throw new IllegalStateException();
        }

        @Override
        public void setMatchingCloseTag(CloseTag closeTag) {
            //no-op
//            throw new IllegalStateException();
        }

        @Override
        public String toString() {
            return new StringBuilder(super.toString()).append(isEmpty() ? "(self close tag)" : "").toString();
        }
        
    }

    static class CommonOpenTag extends EmptyOpenTag {

        private List<Element> children;
        private CloseTag matchingEndTag;
        private int logicalEndOffset;

        public CommonOpenTag(CharSequence source, int startOffset, int endOffset, byte nameLen) {
            super(source, startOffset, endOffset, nameLen);
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public void setMatchingCloseTag(CloseTag endTag) {
            this.matchingEndTag = endTag;
        }

        @Override
        public CloseTag matchingCloseTag() {
            return matchingEndTag;
        }

        @Override
        public void addChild(Element child) {
            if (children == null) {
                children = new ArrayList<Element>(1);
            }
            children.add(child);
            ((ModifiableElement)child).setParent(this);
        }

        @Override
        public void addChildren(Collection<Element> elements) {
            for(Element e : new LinkedList<Element>(elements)) {
                addChild(e);
            }
        }

        @Override
        public void removeChild(Element element) {
            if (children == null) {
                return;
            }
            children.remove(element);
            if (children.isEmpty()) {
                children = null;
            }
            
            ((ModifiableElement)element).setParent(null);
        }

        @Override
        public void removeChildren(Collection<Element> toRemove) {
            if(children == null) {
                return ;
            }
            Iterator<Element> childrenIterator = toRemove.iterator();
            while(childrenIterator.hasNext()) {
                Element child = childrenIterator.next();
                ((ModifiableElement)child).setParent(null);
                childrenIterator.remove();
            }
            if(children().isEmpty()) {
                children = null;
            }
        }

        @Override
        public void insertChildBefore(Element toInsert, Element element) {
            int index = children.indexOf(element);
            if(index == -1) {
                return ;
            }
            children.add(index, toInsert);
            ((ModifiableElement)toInsert).setParent(this);
        }

        @Override
        public Collection<Element> children() {
            return children == null ? Collections.<Element>emptyList() : children;
        }

        @Override
        public Collection<Element> children(ElementType type) {
            Collection<Element> filtered = new ArrayList<Element>();
            for (Element e : children()) {
                if (e.type() == type) {
                    filtered.add(e);
                }
            }
            return filtered;
        }

        @Override
        public Collection<Element> children(ElementFilter filter) {
            Collection<Element> filtered = new ArrayList<Element>();
            for (Element e : children()) {
                if (filter.accepts(e)) {
                    filtered.add(e);
                }
            }
            return filtered;
        }

        @Override
        public <T extends Element> Collection<T> children(Class<T> type) {
            Collection<T> filtered = new ArrayList<T>();
            for (Element child : children()) {
                if (type.isAssignableFrom(child.getClass())) {
                    filtered.add(type.cast(child));
                }
            }
            return filtered;
        }

        @Override
        public void setSemanticEndOffset(int endOffset) {
            this.logicalEndOffset = endOffset;
        }

        @Override
        public int semanticEnd() {
            return logicalEndOffset;
        }
    }

    static class CommonCloseTag extends TagElement implements ModifiableCloseTag {

        private OpenTag matchingOpenTag;

        public CommonCloseTag(CharSequence source, int startOffset, int endOffset, byte nameLen) {
            super(source, startOffset, endOffset, nameLen);
        }

        @Override
        public ElementType type() {
            return ElementType.CLOSE_TAG;
        }

        @Override
        public OpenTag matchingOpenTag() {
            return matchingOpenTag;
        }

        @Override
        public void setMatchingOpenTag(OpenTag openTag) {
            this.matchingOpenTag = openTag;
        }

        @Override
        protected int nameOffsetToStartOffsetDiff() {
            return 2; //"</".length();
        }
    }

    static class VirtualOpenTag implements OpenTag, ModifiableOpenTag {

        private Collection<Attribute> attrs;
        private CharSequence name;
        private List<Element> children;
        private CloseTag matchingCloseTag;
        private int logicalEndOffset;
        private Node parent;

        public VirtualOpenTag(CharSequence name) {
            this.name = name;
        }

        void addAttribute(Attribute attr) {
            if(attrs == null) {
                attrs = new ArrayList<Attribute>(1);
            }
            attrs.add(attr);
        }

        void addAttributes(Collection<Attribute> attributes) {
            if(attrs == null) {
                attrs = new ArrayList<Attribute>(1);
            }
            attrs.addAll(attributes);
        }

        @Override
        public Collection<Attribute> attributes() {
            return attrs == null ? Collections.<Attribute>emptyList() : attrs;
        }

        @Override
        public Collection<Attribute> attributes(AttributeFilter filter) {
            Collection<Attribute> filtered = new ArrayList<Attribute>(1);
            for (Attribute a : attributes()) {
                if (filter.accepts(a)) {
                    filtered.add(a);
                }
            }
            return filtered;
        }

        @Override
        public Attribute getAttribute(String name) {
            //typically very low number of attrs so the linear search doesn't hurt
            for (Attribute a : attributes()) {
                if (LexerUtils.equals(name, a.name(), true, false)) {
                    return a;
                }
            }
            return null;
        }

        @Override
        public boolean isEmpty() {
            return false;
        }

        @Override
        public CloseTag matchingCloseTag() {
            return matchingCloseTag;
        }

        @Override
        public int semanticEnd() {
            return logicalEndOffset;
        }

        @Override
        public CharSequence name() {
            return name;
        }

        @Override
        public CharSequence namespacePrefix() {
            int colonIndex = CharSequences.indexOf(name(), ":");
            return colonIndex == -1 ? null : name().subSequence(0, colonIndex);
        }

        @Override
        public CharSequence unqualifiedName() {
            int colonIndex = CharSequences.indexOf(name(), ":");
            return colonIndex == -1 ? name() : name().subSequence(colonIndex + 1, name().length());
        }

        @Override
        public int from() {
            return -1;
        }

        @Override
        public int to() {
            return -1;
        }

        @Override
        public ElementType type() {
            return ElementType.OPEN_TAG;
        }

        @Override
        public CharSequence image() {
            return null;
        }

        @Override
        public CharSequence id() {
            return name();
        }

        @Override
        public Collection<ProblemDescription> problems() {
            return Collections.emptyList();
        }

        @Override
        public Node parent() {
            return parent;
        }

        @Override
        public Collection<Element> children() {
            return children == null ? Collections.<Element>emptyList() : children;
        }

        @Override
        public Collection<Element> children(ElementType type) {
            Collection<Element> filtered = new ArrayList<Element>();
            for (Element e : children()) {
                if (e.type() == type) {
                    filtered.add(e);
                }
            }
            return filtered;
        }

        @Override
        public Collection<Element> children(ElementFilter filter) {
            Collection<Element> filtered = new ArrayList<Element>();
            for (Element e : children()) {
                if (filter.accepts(e)) {
                    filtered.add(e);
                }
            }
            return filtered;
        }

        @Override
        public <T extends Element> Collection<T> children(Class<T> type) {
            Collection<T> filtered = new ArrayList<T>();
            for (Element child : children()) {
                if (type.isAssignableFrom(child.getClass())) {
                    filtered.add(type.cast(child));
                }
            }
            return filtered;
        }
        
        @Override
        public void insertChildBefore(Element toInsert, Element element) {
            if(children == null) {
                children = new ArrayList<Element>();
            }
            int index = children.indexOf(element);
            if(index == -1) {
                return ;
            }
            children.add(index, toInsert);
            ((ModifiableElement)toInsert).setParent(this);
        }

        @Override
        public void setParent(Node parent) {
            this.parent = parent;
        }

       @Override
        public void addChild(Element child) {
            if (children == null) {
                children = new ArrayList<Element>(1);
            }
            children.add(child);
            ((ModifiableElement)child).setParent(this);
        }

        @Override
        public void addChildren(Collection<Element> elements) {
            for(Element e : new LinkedList<Element>(elements)) {
                addChild(e);
            }
        }

        @Override
        public void removeChild(Element element) {
            if (children == null) {
                return;
            }
            children.remove(element);
            if (children.isEmpty()) {
                children = null;
            }
            
            ((ModifiableElement)element).setParent(null);
        }

        @Override
        public void removeChildren(Collection<Element> toRemove) {
            if(children == null) {
                return ;
            }
            Iterator<Element> childrenIterator = toRemove.iterator();
            while(childrenIterator.hasNext()) {
                Element child = childrenIterator.next();
                ((ModifiableElement)child).setParent(null);
                childrenIterator.remove();
            }
            if(children().isEmpty()) {
                children = null;
            }
        }

        @Override
        public void setEndOffset(int endOffset) {
            //no-op
        }

        @Override
        public void detachFromParent() {
            ModifiableOpenTag mot = (ModifiableOpenTag) parent();
            if(mot != null) {
                mot.removeChild(this);
                setParent(null);
            }
        }

        @Override
        public void setSemanticEndOffset(int endOffset) {
            this.logicalEndOffset = endOffset;
        }

        @Override
        public void setAttribute(Attribute attribute) {
            addAttribute(attribute);
        }

        @Override
        public void setMatchingCloseTag(CloseTag closeTag) {
            this.matchingCloseTag = closeTag;
        }
        
        @Override
        public String toString() {
            return new StringBuilder().append(name()).append("(").append(type().name()).append(")").append("; (virtual)").toString();
        }

    }

    //it would be better design/memory usage not to reuse commonopentag, but since there's just
    //one instance some of the spare fields are acceptable
    static class Root extends CommonOpenTag implements FeaturedNode {

        private static final String ROOT = "root"; //NOI18N
        private String namespace;

        public Root(CharSequence source, String namespace) {
            super(source, 0, source.length(), (byte) 0);
            
            this.namespace = namespace;
        }

        @Override
        public CharSequence name() {
            return ROOT;
        }

        //must be overridden since the TagElement class uses 'from' - 'to' diff in short value,
        @Override
        public int to() {
            return source.length();
        }
        
        @Override
        public ElementType type() {
            return ElementType.ROOT;
        }

        @Override
        public Object getProperty(String propertyName) {
            if (propertyName.equalsIgnoreCase("namespace")) { //NOI18N
                return namespace;
            }

            return null;
        }
    }

    static class CommonAttribute implements Attribute {

        private CharSequence source;
        private int nameOffset;
        private short valueOffset2nameOffsetDiff;
        private short nameLen;
        private int valueLen;

        public CommonAttribute(CharSequence source, int nameOffset, short nameLen) {
            this.source = source;

            this.nameOffset = nameOffset;
            this.valueOffset2nameOffsetDiff = -1;

            this.nameLen = nameLen;
            this.valueLen = -1;
        }

        public CommonAttribute(CharSequence source, int nameOffset, int valueOffset, short nameLen, int valueLen) {
            this.source = source;

            this.nameOffset = nameOffset;
            this.valueOffset2nameOffsetDiff = (short) (valueOffset - nameOffset);

            this.nameLen = nameLen;
            this.valueLen = valueLen;
        }

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

        @Override
        public CharSequence name() {
            return source.subSequence(nameOffset, nameOffset + nameLen);
        }

        @Override
        public int valueOffset() {
            return valueOffset2nameOffsetDiff == -1 ? -1 : nameOffset + valueOffset2nameOffsetDiff;
        }

        @Override
        public CharSequence value() {
            if(valueLen == -1) {
                return null;
            }
            //Bug 221821 - StringIndexOutOfBoundsException: String index out of range: 234
            int diff = source.length() - valueOffset();
            if(diff < 0) {
                assert false : String.format("valueOffset:%s > source.length:%s", 
                        valueOffset(), source.length()); //report in dev builds
                return null;
            }
            if(diff < valueLen) {
                assert false : String.format("valueOffset:%s + valueLen:%s > source.length:%s", 
                        valueOffset(), valueLen, source.length()); //report in dev builds
                return null;
            }
            
            return source.subSequence(valueOffset(), valueOffset() + valueLen);
        }

        @Override
        public boolean isValueQuoted() {
            if(value() == null) {
                return false;
            }
            if (valueLen < 2) {
                return false;
            } else {
                CharSequence value = value();
                return ((value.charAt(0) == '\'' || value.charAt(0) == '"')
                        && (value.charAt(value.length() - 1) == '\'' || value.charAt(value.length() - 1) == '"'));
            }
        }

        @Override
        public CharSequence unquotedValue() {
            if(value() == null) {
                return null;
            }
            return isValueQuoted() ? value().subSequence(1, value().length() - 1) : value();
        }

        @Override
        public CharSequence namespacePrefix() {
            int colonIndex = CharSequences.indexOf(name(), ":");
            return colonIndex == -1 ? null : name().subSequence(0, colonIndex);
        }

        @Override
        public CharSequence unqualifiedName() {
            int colonIndex = CharSequences.indexOf(name(), ":");
            return colonIndex == -1 ? name() : name().subSequence(colonIndex + 1, name().length());
        }

        @Override
        public int from() {
            return nameOffset();
        }

        @Override
        public int to() {
            return value() != null
                    ? valueOffset() + valueLen
                    : nameOffset() + nameLen;
        }

        @Override
        public ElementType type() {
            return ElementType.ATTRIBUTE;
        }

        @Override
        public CharSequence image() {
            return source.subSequence(from(), to());
        }

        @Override
        public CharSequence id() {
            return type().name();
        }

        @Override
        public Collection<ProblemDescription> problems() {
            return Collections.emptyList();
        }

        @Override
        public Node parent() {
            return null;
        }
    }
}
