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

import java.util.*;
import java.util.logging.Logger;
import org.netbeans.modules.html.editor.lib.api.HelpItem;
import org.netbeans.modules.html.editor.lib.api.HtmlVersion;
import org.netbeans.modules.html.editor.lib.api.model.*;
import org.netbeans.modules.html.editor.lib.dtd.DTD;
import org.openide.util.lookup.ServiceProvider;

/**
 * Provides {@link HtmlModel} for HTML 3.2 to 4.01
 *
 * @author marekfukala
 */
@ServiceProvider(service = HtmlModelProvider.class, position = 20)
public class Html4ModelProvider implements HtmlModelProvider {

    private static final Map<HtmlVersion, Collection<HtmlTag>> ALL_TAGS_MAP = new EnumMap<>(HtmlVersion.class);

    private static synchronized Collection<HtmlTag> getAllTags(HtmlVersion version) {
        Collection<HtmlTag> value = ALL_TAGS_MAP.get(version);
        if (value == null) {
            DTD dtd = version.getDTD();
            assert dtd != null;

            List<org.netbeans.modules.html.editor.lib.dtd.DTD.Element> all = dtd.getElementList("");
            value = new ArrayList<>();
            for (org.netbeans.modules.html.editor.lib.dtd.DTD.Element e : all) {
                value.add(DTD2HtmlTag.getTagForElement(dtd, e));
            }
            ALL_TAGS_MAP.put(version, value);
        }
        return value;
    }

    @Override
    public HtmlModel getModel(final HtmlVersion version) {
        switch (version) {
            case HTML32:
            case HTML40_FRAMESET:
            case HTML40_STRICT:
            case HTML40_TRANSATIONAL:
            case HTML41_FRAMESET:
            case HTML41_STRICT:
            case HTML41_TRANSATIONAL:
            case XHTML10_FRAMESET:
            case XHTML10_STICT:
            case XHTML10_TRANSATIONAL:
            case XHTML11:
                return new Html4Model(version);
            default:
                return null;
        }
    }

    private static class Html4Model implements HtmlModel {

        private HtmlVersion version;

        public Html4Model(HtmlVersion version) {
            this.version = version;
        }

        @Override
        public Collection<HtmlTag> getAllTags() {
            return Html4ModelProvider.getAllTags(version);
        }

        @Override
        public HtmlTag getTag(String tagName) {
            DTD.Element element = version.getDTD().getElement(tagName);
            if (element == null) {
                return null;
            }
            return DTD2HtmlTag.getTagForElement(version.getDTD(), element);
        }

        @Override
        public Collection<NamedCharRef> getNamedCharacterReferences() {
            return version.getDTD().getCharRefList("");
        }

        @Override
        public String getModelId() {
            return "html4model"; //NOI18N
        }
    }

    private static class DTD2HtmlTag {

        private static final Logger LOGGER = Logger.getLogger(DTD2HtmlTag.class.getName());
        private static HashMap<DTD.Element, HtmlTag> MAP = new HashMap<>();
        private static HashMap<DTD.Attribute, HtmlTagAttribute> ATTRS_MAP = new HashMap<>();

        private static synchronized HtmlTag getTagForElement(DTD dtd, DTD.Element elementName) {
            HtmlTag impl = MAP.get(elementName);
            if (impl == null) {
                impl = new DTDElement2HtmlTagAdapter(dtd, elementName);
                MAP.put(elementName, impl);
            }
            return impl;
        }

        private static Collection<HtmlTag> convert(DTD dtd, Collection<DTD.Element> elements) {
            Collection<HtmlTag> converted = new ArrayList<>();
            for (DTD.Element element : elements) {
                assert element != null;
                converted.add(getTagForElement(dtd, element));
            }
            return converted;
        }

        private static synchronized HtmlTagAttribute getHtmlTagAttribute(DTD.Attribute attribute) {
            HtmlTagAttribute attr = ATTRS_MAP.get(attribute);
            if (attr == null) {
                attr = new Attribute2HtmlTagAttribute(attribute);
                ATTRS_MAP.put(attribute, attr);
            }
            return attr;
        }

        private static class DTDElement2HtmlTagAdapter implements HtmlTag {

            private DTD.Element element;
            private DTD dtd; //needed just because of the html-body child hack
            private Collection<HtmlTagAttribute> attrs;
            private Collection<HtmlTag> children;

            private DTDElement2HtmlTagAdapter(DTD dtd, DTD.Element element) {
                this.dtd = dtd;
                this.element = element;
                this.attrs = wrap(element.getAttributeList(null));
            }

            private Collection<HtmlTagAttribute> wrap(Collection<DTD.Attribute> attrNames) {
                if (attrNames == null) {
                    return Collections.emptyList();
                }
                Collection<HtmlTagAttribute> attributes = new LinkedList<>();
                for (DTD.Attribute an : attrNames) {
                    HtmlTagAttribute hta = getHtmlTagAttribute(an);
                    if (hta != null) {
                        attributes.add(hta);
                    } else {
                        LOGGER.info("Unknown attribute " + an + " requested.");//NOI18N
                    }
                }
                return attributes;
            }

            @Override
            public String getName() {
                return element.getName();
            }

            @Override
            public Collection<HtmlTagAttribute> getAttributes() {
                return attrs;
            }

            @Override
            public boolean isEmpty() {
                return element.isEmpty();
            }

            @Override
            public boolean hasOptionalOpenTag() {
                return element.hasOptionalStart();
            }

            @Override
            public boolean hasOptionalEndTag() {
                return element.hasOptionalEnd();
            }

            @Override
            public HtmlTagAttribute getAttribute(String name) {
                DTD.Attribute attr = element.getAttribute(name);
                if (attr == null) {
                    return null;
                }
                return getHtmlTagAttribute(attr);
            }

            @Override
            public HtmlTagType getTagClass() {
                return HtmlTagType.HTML;
            }

            @Override
            public synchronized Collection<HtmlTag> getChildren() {
                //logic copied from David Konecny's HtmlIndenter.
                if (children == null) {
                    Set<DTD.Element> set = new HashSet<>();
                    for (DTD.Element el : (Set<DTD.Element>) element.getContentModel().getIncludes()) {
                        if (el != null) {
                            set.add(el);
                        }
                    }
                    for (DTD.Element el : (Set<DTD.Element>) element.getContentModel().getExcludes()) {
                        if (el != null) {
                            set.remove(el);
                        }
                    }
                    for (DTD.Element el : (Set<DTD.Element>) element.getContentModel().getContent().getPossibleElements()) {
                        if (el != null) {
                            set.add(el);
                        }
                    }
                    if (element.getName().equalsIgnoreCase("HTML")) {
                        // XXXXXXXXXXXXXXXXX TODO:
                        DTD.Element bodyElement = dtd.getElement("BODY");
                        if(bodyElement != null) {
                            set.add(bodyElement);
                        }
                    }
                    children = convert(dtd, set);
                }
                return children;
            }

            @Override
            public HelpItem getHelp() {
                return null;
            }
        }

        private static class Attribute2HtmlTagAttribute implements HtmlTagAttribute {

            private DTD.Attribute attr;

            public Attribute2HtmlTagAttribute(DTD.Attribute attr) {
                this.attr = attr;
            }

            @Override
            public String getName() {
                return attr.getName();
            }

            @Override
            public boolean isRequired() {
                return attr.isRequired();
            }

            @Override
            public HtmlTagAttributeType getType() {
                switch (attr.getType()) {
                    case DTD.Attribute.TYPE_BOOLEAN:
                        return HtmlTagAttributeType.BOOLEAN;
                    case DTD.Attribute.TYPE_SET:
                        return HtmlTagAttributeType.SET;
                    case DTD.Attribute.TYPE_BASE:
                        return HtmlTagAttributeType.GENERIC;
                    default:
                        return HtmlTagAttributeType.GENERIC;
                }
            }

            @Override
            public Collection<String> getPossibleValues() {
                Collection<DTD.Value> values = attr.getValueList(null);
                if (values == null) {
                    return Collections.emptyList();
                }
                Collection<String> res = new LinkedList<>();
                for (DTD.Value v : values) {
                    res.add(v.getName());
                }
                return res;
            }

            @Override
            public HelpItem getHelp() {
                return null;
            }
        }
    }
}
