package org.codehaus.modello.model;

/*
 * Copyright (c) 2004, Codehaus.org
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is furnished to do
 * so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.codehaus.modello.ModelloRuntimeException;
import org.codehaus.modello.metadata.ModelMetadata;
import org.codehaus.modello.plugin.model.ModelClassMetadata;
import org.codehaus.plexus.util.StringUtils;

/**
 * @author <a href="mailto:jason@modello.org">Jason van Zyl</a>
 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
 */
public class Model extends BaseElement {
    private String id;

    private List<ModelClass> classes = new ArrayList<>();

    private List<ModelDefault> defaults = new ArrayList<>();

    private List<ModelInterface> interfaces = new ArrayList<>();

    private transient Map<String, List<ModelClass>> classMap = new HashMap<>();

    private transient Map<String, ModelDefault> defaultMap = new HashMap<>();

    private transient Map<String, List<ModelInterface>> interfaceMap = new HashMap<>();

    private VersionDefinition versionDefinition;

    public Model() {
        super(true);
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public VersionDefinition getVersionDefinition() {
        return versionDefinition;
    }

    public void setVersionDefinition(VersionDefinition versionDefinition) {
        this.versionDefinition = versionDefinition;
    }

    public ModelMetadata getMetadata(String key) {
        return getMetadata(ModelMetadata.class, key);
    }

    public String getRoot(Version version) {
        List<ModelClass> classes = getClasses(version);

        String className = null;

        for (ModelClass currentClass : classes) {
            ModelClassMetadata metadata = null;

            try {
                metadata = (ModelClassMetadata) currentClass.getMetadata(ModelClassMetadata.ID);
            } catch (Exception e) {
            }

            if (metadata != null && metadata.isRootElement()) {
                if (className == null) {
                    className = currentClass.getName();
                } else {
                    throw new ModelloRuntimeException(
                            "There are more than one class as root element for this version " + version + ".");
                }
            }
        }

        if (className == null) {
            throw new ModelloRuntimeException("There aren't root element for version " + version + ".");
        }

        return className;
    }

    /**
     * @deprecated This shouldn't be used, anything querying the model should read the
     *             package of the class. Use getDefaultPackageName(..).
     * @param withVersion return the version
     * @param version the version
     * @return the package name
     *
     */
    @Deprecated
    public String getPackageName(boolean withVersion, Version version) {
        return getDefaultPackageName(withVersion, version);
    }

    public List<ModelClass> getAllClasses() {
        return classes;
    }

    public List<ModelClass> getClasses(Version version) {
        List<ModelClass> classList = new ArrayList<>();

        for (ModelClass currentClass : classes) {
            if (version.inside(currentClass.getVersionRange())) {
                classList.add(currentClass);
            }
        }

        return classList;
    }

    public ModelClass getClass(String type, Version version, boolean optionnal) {
        return getClass(type, new VersionRange(version), optionnal);
    }

    public ModelClass getClass(String type, Version version) {
        return getClass(type, new VersionRange(version), false);
    }

    public ModelClass getClass(String type, VersionRange versionRange, boolean optionnal) {
        ModelClass value = getModelClass(type, versionRange);

        if (value != null) {
            return value;
        }
        if (optionnal) {
            return null;
        }
        throw new ModelloRuntimeException(
                "There is no class '" + type + "' in the version range '" + versionRange.toString() + "'.");
    }

    public ModelClass getClass(String type, VersionRange versionRange) {
        ModelClass value = getModelClass(type, versionRange);

        if (value != null) {
            return value;
        }

        throw new ModelloRuntimeException(
                "There is no class '" + type + "' in the version range '" + versionRange.toString() + "'.");
    }

    public boolean hasClass(String type, Version version) {
        ModelClass value = getModelClass(type, new VersionRange(version));

        return value != null;
    }

    private ModelClass getModelClass(String type, VersionRange versionRange) {
        List<ModelClass> classList = classMap.get(type);

        ModelClass value = null;
        if (classList != null) {
            for (ModelClass modelClass : classList) {
                if (versionRange.getFromVersion().inside(modelClass.getVersionRange())
                        && versionRange.getToVersion().inside(modelClass.getVersionRange())) {
                    value = modelClass;
                }
            }
        }
        return value;
    }

    public void addClass(ModelClass modelClass) {
        if (classMap.containsKey(modelClass.getName())) {
            List<ModelClass> classList = classMap.get(modelClass.getName());

            for (ModelClass currentClass : classList) {
                if (VersionUtil.isInConflict(modelClass.getVersionRange(), currentClass.getVersionRange())) {
                    throw new ModelloRuntimeException("Duplicate class: " + modelClass.getName() + ".");
                }
            }
        } else {
            List<ModelClass> classList = new ArrayList<>();

            classMap.put(modelClass.getName(), classList);
        }

        getAllClasses().add(modelClass);

        classMap.get(modelClass.getName()).add(modelClass);
    }

    // ----------------------------------------------------------------------
    // Defaults
    // ----------------------------------------------------------------------

    public List<ModelDefault> getDefaults() {
        return defaults;
    }

    public ModelDefault getDefault(String key) {
        ModelDefault modelDefault = (ModelDefault) defaultMap.get(key);

        if (modelDefault == null) {
            try {
                modelDefault = ModelDefault.getDefault(key);
            } catch (ModelValidationException mve) {
                throw new ModelloRuntimeException(mve.getMessage(), mve);
            }
        }

        return modelDefault;
    }

    public void addDefault(ModelDefault modelDefault) {
        if (defaultMap.containsKey(modelDefault.getKey())) {
            throw new ModelloRuntimeException("Duplicate default: " + modelDefault.getKey() + ".");
        }

        getDefaults().add(modelDefault);

        defaultMap.put(modelDefault.getKey(), modelDefault);
    }

    public String getDefaultPackageName(boolean withVersion, Version version) {
        String packageName = getDefault(ModelDefault.PACKAGE).getValue();

        if (withVersion) {
            packageName += "." + version.toString("v", "_");
        }

        return packageName;
    }

    // ----------------------------------------------------------------------
    //
    // ----------------------------------------------------------------------

    public List<ModelInterface> getAllInterfaces() {
        return interfaces;
    }

    public List<ModelInterface> getInterfaces(Version version) {
        List<ModelInterface> interfaceList = new ArrayList<>();

        for (ModelInterface currentInterface : interfaces) {
            if (version.inside(currentInterface.getVersionRange())) {
                interfaceList.add(currentInterface);
            }
        }

        return interfaceList;
    }

    public ModelInterface getInterface(String type, Version version) {
        return getInterface(type, new VersionRange(version));
    }

    public ModelInterface getInterface(String type, VersionRange versionRange) {
        ModelInterface value = getModelInterface(type, versionRange);

        if (value != null) {
            return value;
        }

        throw new ModelloRuntimeException(
                "There is no interface '" + type + "' in the version range '" + versionRange.toString() + "'.");
    }

    private ModelInterface getModelInterface(String type, VersionRange versionRange) {
        List<ModelInterface> interfaceList = interfaceMap.get(type);

        if (interfaceList != null) {
            for (ModelInterface modelInterface : interfaceList) {
                if (versionRange.getFromVersion().inside(modelInterface.getVersionRange())
                        && versionRange.getToVersion().inside(modelInterface.getVersionRange())) {
                    return modelInterface;
                }
            }
        }

        return null;
    }

    public void addInterface(ModelInterface modelInterface) {
        if (interfaceMap.containsKey(modelInterface.getName())) {
            List<ModelInterface> interfaceList = interfaceMap.get(modelInterface.getName());

            for (ModelInterface currentInterface : interfaceList) {
                if (VersionUtil.isInConflict(modelInterface.getVersionRange(), currentInterface.getVersionRange())) {
                    throw new ModelloRuntimeException("Duplicate interface: " + modelInterface.getName() + ".");
                }
            }
        } else {
            List<ModelInterface> interfaceList = new ArrayList<>();

            interfaceMap.put(modelInterface.getName(), interfaceList);
        }

        getAllInterfaces().add(modelInterface);

        interfaceMap.get(modelInterface.getName()).add(modelInterface);
    }

    public ModelType getType(String type, Version version) {
        return getType(type, new VersionRange(version));
    }

    public ModelType getType(String type, VersionRange versionRange) {
        ModelType value = getModelClass(type, versionRange);

        if (value != null) {
            return value;
        }

        value = getModelInterface(type, versionRange);

        if (value != null) {
            return value;
        }

        throw new ModelloRuntimeException("There is no class or interface '" + type + "' in the version range '"
                + versionRange.toString() + "'.");
    }

    public void initialize() {
        for (ModelClass modelClass : classes) {
            modelClass.initialize(this);
        }

        for (ModelInterface modelInterface : interfaces) {
            modelInterface.initialize(this);
        }
    }

    public void validateElement() {}

    public ModelClass getLocationTracker(Version version) {
        List<ModelClass> modelClasses = getClasses(version);

        ModelClass locationTracker = null;

        for (ModelClass modelClass : modelClasses) {
            ModelClassMetadata metadata = (ModelClassMetadata) modelClass.getMetadata(ModelClassMetadata.ID);

            if (metadata != null && StringUtils.isNotEmpty(metadata.getLocationTracker())) {
                if (locationTracker == null) {
                    locationTracker = modelClass;
                } else {
                    throw new ModelloRuntimeException(
                            "There are multiple location tracker classes (" + locationTracker.getName() + " vs. "
                                    + modelClass.getName() + ") for this version " + version + ".");
                }
            }
        }

        return locationTracker;
    }

    public ModelClass getSourceTracker(Version version) {
        List<ModelClass> modelClasses = getClasses(version);

        ModelClass sourceTracker = null;

        for (ModelClass modelClass : modelClasses) {
            ModelClassMetadata metadata = (ModelClassMetadata) modelClass.getMetadata(ModelClassMetadata.ID);

            if (metadata != null && StringUtils.isNotEmpty(metadata.getSourceTracker())) {
                if (sourceTracker == null) {
                    sourceTracker = modelClass;
                } else {
                    throw new ModelloRuntimeException(
                            "There are multiple source tracker classes (" + sourceTracker.getName() + " vs. "
                                    + modelClass.getName() + ") for this version " + version + ".");
                }
            }
        }

        return sourceTracker;
    }
}
