/*
 * Copyright 2016 the original author or authors.
 *
 * Licensed 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.gradle.plugins.ide.eclipse.model;

import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import groovy.lang.GroovyObjectSupport;
import groovy.util.Node;
import groovy.util.NodeList;
import org.gradle.plugins.ide.eclipse.model.internal.PathUtil;

import java.util.Map;
import java.util.Set;

// TODO: consider entryAttributes in equals, hashCode, and toString

/**
 * Common superclass for all {@link ClasspathEntry} instances.
 */
public abstract class AbstractClasspathEntry extends GroovyObjectSupport implements ClasspathEntry {
    private static final String NATIVE_LIBRARY_ATTRIBUTE = "org.eclipse.jdt.launching.CLASSPATH_ATTR_LIBRARY_PATH_ENTRY";
    public static final String COMPONENT_NON_DEPENDENCY_ATTRIBUTE = "org.eclipse.jst.component.nondependency";
    public static final String COMPONENT_DEPENDENCY_ATTRIBUTE = "org.eclipse.jst.component.dependency";

    protected String path;
    protected boolean exported;
    protected Set<AccessRule> accessRules;
    protected final Map<String, Object> entryAttributes;

    public AbstractClasspathEntry(Node node) {
        path = normalizePath((String) node.attribute("path"));
        this.exported = isNodeExported(node);
        accessRules = readAccessRules(node);
        entryAttributes = readEntryAttributes(node);
        Preconditions.checkNotNull(path);
        Preconditions.checkNotNull(accessRules);
    }

    private boolean isNodeExported(Node node) {
        Object value = node.attribute("exported");
        if (value == null) {
            return false;
        } else if (value instanceof Boolean) {
            return ((Boolean) value).booleanValue();
        } else {
            return Boolean.valueOf((String) value);
        }
    }

    public AbstractClasspathEntry(String path) {
        Preconditions.checkNotNull(path);
        this.path = normalizePath(path);
        this.exported = false;
        this.accessRules = Sets.newHashSet();
        entryAttributes = Maps.newHashMap();
    }

    public String getPath() {
        return path;
    }

    public void setPath(String path) {
        this.path = path;
    }

    // TODO both the getExported and isExported is called in the code; we should unify that

    public boolean isExported() {
        return exported;
    }

    public boolean getExported() {
        return exported;
    }

    public void setExported(boolean exported) {
        this.exported = exported;
    }

    public Set<AccessRule> getAccessRules() {
        return accessRules;
    }

    public void setAccessRules(Set<AccessRule> accessRules) {
        this.accessRules = accessRules;
    }

    public Map<String, Object> getEntryAttributes() {
        return entryAttributes;
    }

    public String getNativeLibraryLocation() {
        return (String) entryAttributes.get(NATIVE_LIBRARY_ATTRIBUTE);
    }

    public void setNativeLibraryLocation(String location) {
        entryAttributes.put(NATIVE_LIBRARY_ATTRIBUTE, location);
    }

    @Override
    public void appendNode(Node node) {
        addClasspathEntry(node, Maps.<String, Object>newHashMap());
    }

    protected Node addClasspathEntry(Node node, Map<String, ?> attributes) {
        Map<String, Object> allAttributes = Maps.newHashMap();
        for (String key : attributes.keySet()) {
            Object value = attributes.get(key);
            if (value != null && !String.valueOf(value).isEmpty()) {
                allAttributes.put(key, value);
            }
        }
        allAttributes.put("kind", getKind());
        allAttributes.put("path", path);


        if (exported && !(this instanceof SourceFolder)) {
            allAttributes.put("exported", true);
        }

        Node entryNode = node.appendNode("classpathentry", allAttributes);
        writeAccessRules(entryNode);
        writeEntryAttributes(entryNode);
        return entryNode;
    }

    protected String normalizePath(String path) {
        return PathUtil.normalizePath(path);
    }

    private Set<AccessRule> readAccessRules(Node node) {
        Set<AccessRule> accessRules = Sets.newHashSet();
        NodeList accessRulesNodes = (NodeList) node.get("accessrules");
        for (Object accessRulesNode : accessRulesNodes) {
            NodeList accessRuleNodes = (NodeList) ((Node) accessRulesNode).get("accessrule");
            for (Object accessRuleNode : accessRuleNodes) {
                Node ruleNode = (Node) accessRuleNode;
                accessRules.add(new AccessRule((String) ruleNode.attribute("kind"), (String) ruleNode.attribute("pattern")));
            }
        }
        return accessRules;
    }

    private void writeAccessRules(Node node) {
        if (accessRules == null || accessRules.isEmpty()) {
            return;
        }
        Node accessRulesNode;
        NodeList accessRulesNodes = (NodeList) node.get("accessrules");
        if (accessRulesNodes.size() == 0) {
            accessRulesNode = node.appendNode("accessrules");
        } else {
            accessRulesNode = (Node) accessRulesNodes.get(0);
        }
        for (AccessRule rule : accessRules) {
            Map<String, Object> attributes  = Maps.newHashMap();
            attributes.put("kind", rule.getKind());
            attributes.put("pattern", rule.getPattern());
            accessRulesNode.appendNode("accessrule", attributes);
        }
    }

    private Map<String, Object> readEntryAttributes(Node node) {
        Map<String, Object> attributes = Maps.newHashMap();
        NodeList attributesNodes = (NodeList) node.get("attributes");
        for (Object attributesEntry : attributesNodes) {
            NodeList attributeNodes = (NodeList) ((Node) attributesEntry).get("attribute");
            for (Object attributeEntry : attributeNodes) {
                Node attributeNode = (Node) attributeEntry;
                attributes.put((String) attributeNode.attribute("name"), attributeNode.attribute("value"));
            }
        }
        return attributes;
    }

    public void writeEntryAttributes(Node node) {
        Map<String, Object> effectiveEntryAttrs = Maps.newHashMap();
        for (String key : entryAttributes.keySet()) {
            Object value = entryAttributes.get(key);
            if ((value != null && !String.valueOf(value).isEmpty()) || COMPONENT_NON_DEPENDENCY_ATTRIBUTE.equals(key)) {
                effectiveEntryAttrs.put(key, value);
            }
        }

        if (effectiveEntryAttrs.isEmpty()) {
            return;
        }

        if (effectiveEntryAttrs.containsKey(COMPONENT_DEPENDENCY_ATTRIBUTE)
            && effectiveEntryAttrs.containsKey(COMPONENT_NON_DEPENDENCY_ATTRIBUTE)) {
            //For conflicting component dependency entries, the non-dependency loses
            //because it is our default and it means the user has configured something else.
            effectiveEntryAttrs.remove(COMPONENT_NON_DEPENDENCY_ATTRIBUTE);
        }

        Node attributesNode;
        NodeList attributesNodes = (NodeList) node.get("attributes");
        if (attributesNodes.size() == 0) {
            attributesNode = node.appendNode("attributes");
        } else {
            attributesNode = (Node) attributesNodes.get(0);
        }

        for (String key : effectiveEntryAttrs.keySet()) {
            Object value = effectiveEntryAttrs.get(key);
            Map<String, Object> attrs = Maps.newHashMap();
            attrs.put("name", key);
            attrs.put("value", value);
            attributesNode.appendNode("attribute", attrs);
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        AbstractClasspathEntry that = (AbstractClasspathEntry) o;
        return exported == that.exported
            && Objects.equal(path, that.path)
            && Objects.equal(accessRules, that.accessRules)
            && Objects.equal(getNativeLibraryLocation(), that.getNativeLibraryLocation());
    }

    @Override
    public int hashCode() {
        return Objects.hashCode(path, exported, accessRules, getNativeLibraryLocation());
    }

    @Override
    public String toString() {
        return "{path='" + path + "', nativeLibraryLocation='" + getNativeLibraryLocation() + "', exported=" + exported + ", accessRules=" + accessRules + "}";
    }
}

