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 org.codehaus.modello.ModelloRuntimeException;
import org.codehaus.modello.metadata.ModelMetadata;
import org.codehaus.modello.plugin.model.ModelClassMetadata;
import org.codehaus.plexus.util.StringUtils;

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

/**
 * @author <a href="mailto:jason@modello.org">Jason van Zyl</a>
 * @author <a href="mailto:evenisse@codehaus.org">Emmanuel Venisse</a>
 * @version $Id: Model.java 1459 2010-04-19 10:36:55Z bentmann $
 */
public class Model
    extends BaseElement
{
    private String id;

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

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

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

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

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

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

    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(..).
     */
    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<ModelClass>();

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

        return classList;
    }

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

    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<ModelClass>();

            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<ModelInterface>();

        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<ModelInterface>();

            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;
    }

}
