/*
 * Copyright (c) 2010, Stanislav Muhametsin. All Rights Reserved.
 *
 * 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.qi4j.index.sql.support.common;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.qi4j.api.association.AssociationDescriptor;
import org.qi4j.api.common.QualifiedName;
import org.qi4j.api.property.PropertyDescriptor;

/**
 * A helper interface to encapsulate information about qualified name and how it appears in
 * database.
 *
 * @author Stanislav Muhametsin
 */
public final class QNameInfo
{
    /**
     * Currently all possible types of qualified names: {@link #PROPERTY} for properties,
     * {@link #ASSOCIATION} for associations, and {@link #MANY_ASSOCIATION} for many-associations.
     */
    public enum QNameType
    {
        PROPERTY,
        ASSOCIATION,
        MANY_ASSOCIATION
    }

    private static final List<Class<?>> EMPTY_COLL_CLASSES = new ArrayList<>();

    private String _tableName;

    /**
     * Store also information about collection types - in case it is required in future.
     */
    private final List<Class<?>> _collectionClasses;

    private final QualifiedName _qName;

    private final QNameType _qNameType;

    private final Type _finalType;

    private final Boolean _isFinalTypePrimitive;

    private final PropertyDescriptor _propertyDescriptor;

    private final AssociationDescriptor _associationDescriptor;

    private final AssociationDescriptor _manyAssociationDescriptor;

    private QNameInfo( QualifiedName qName, //
                       QNameType qNameType, //
                       List<Class<?>> collectionClasses, //
                       String tableName, //
                       Type finalType, //
                       PropertyDescriptor propertyDescriptor, //
                       AssociationDescriptor associationDescriptor, //
                       AssociationDescriptor manyAssociationDescriptor //
    )
    {
        if( ( propertyDescriptor != null && associationDescriptor == null && manyAssociationDescriptor == null )
            || ( propertyDescriptor == null && associationDescriptor != null && manyAssociationDescriptor == null )
            || ( propertyDescriptor == null && associationDescriptor == null && manyAssociationDescriptor != null ) )
        {
            this._propertyDescriptor = propertyDescriptor;
            this._associationDescriptor = associationDescriptor;
            this._manyAssociationDescriptor = manyAssociationDescriptor;
        }
        else
        {
            throw new IllegalArgumentException(
                "Exactly one of property, association, or many-association descriptors must be non-null." );
        }
        this._qName = qName;
        this._qNameType = qNameType;
        this._collectionClasses = ( collectionClasses == null || collectionClasses.isEmpty()
                                    ? EMPTY_COLL_CLASSES
                                    : collectionClasses );
        this._tableName = tableName;
        this._finalType = finalType;
        Boolean isFinalTypePrimitive = false;
        if( finalType instanceof Class<?> )
        {
            Class<?> finalClass = (Class<?>) finalType;
            isFinalTypePrimitive = //
                Number.class.isAssignableFrom( finalClass )//
                || Boolean.class.isAssignableFrom( finalClass ) //
                || Character.class.isAssignableFrom( finalClass ) //
                || Date.class.isAssignableFrom( finalClass ) //
                || Enum.class.isAssignableFrom( finalClass ) //
                || String.class.isAssignableFrom( finalClass )//
                ;
        }
        this._isFinalTypePrimitive = isFinalTypePrimitive;
    }

    /**
     * If qualified name represented by this interface is a property with collection as type,
     * returns the {@code amount of nested collections + 1}. That is, assuming {@code X} is not a
     * collection, for type {@code Property<Set<X>>} this returns {@code 1}, for
     * {@code Property<Set<Set<X>>>} this returns {@code 2}, etc. If qualified name represented by
     * this interface is not a property or a property with no collection type, this method returns
     * {@code 0}.
     *
     * @return The collection depth ({@code > 0}) of qualified name, if this interface represents
     *         qualified name with collection property; {@code 0} otherwise.
     */
    public Integer getCollectionDepth()
    {
        return this._collectionClasses.size();
    }

    /**
     * Returns the non-collection type of this qualified name. That is, for {@code Property<X>} this
     * returns {@code X} if {@code X} is not a collection type, and for {@code Property<Set<Y>>}
     * this returns {@code Y} if {@code Y} is not a collection type.
     *
     * @return The non-collection type of this qualified name.
     */
    public Type getFinalType()
    {
        return this._finalType;
    }

    /**
     * Gets the qualified name this interface represents.
     *
     * @return The qualified name this interface represents.
     * @see QualifiedName
     */
    public QualifiedName getQName()
    {
        return this._qName;
    }

    /**
     * Gets the table name in database, used to store values of the qualified name this interface
     * represents.
     *
     * @return The table name in database, used to store values of the qualified name this interface
     *         represents. May be {@code null} if it is not yet decided.
     */
    public String getTableName()
    {
        return this._tableName;
    }

    /**
     * Sets the previously undecided table name to some specific one. This method only works when
     * argument is {@code non-null} and current table name is {@code null}.
     *
     * @param tableName The new table name. Must be {@code non-null}.
     * @throws IllegalArgumentException If {@code tableName} is {@code null}.
     * @throws IllegalStateException If current table name is {@code non-null}.
     */
    public void setTableName( String tableName )
    {
        if( tableName == null )
        {
            throw new IllegalArgumentException( "Table name must not be null." );
        }
        if( this._tableName == null )
        {
            this._tableName = tableName;
        }
        else
        {
            throw new IllegalStateException( "Can only set table name when it is null." );
        }
    }

    /**
     * Returns whether the final (non-collection) type of this qualified name is not seen as value
     * composite. Always returns {@code false} for qualified names of type
     * {@link QNameType#ASSOCIATION} and {@link QNameType#MANY_ASSOCIATION}.
     *
     * @return {@code true} if {@link #getFinalType()} is not seen as value composite type;
     *         {@code false} otherwise.
     */
    public Boolean isFinalTypePrimitive()
    {
        return this._isFinalTypePrimitive;
    }

    /**
     * Returns {@link PropertyDescriptor} associated with this property, if this qualified name info
     * represents a property. Returns {@code null} otherwise.
     *
     * @return {@link PropertyDescriptor} if this qualified name info is associated with property,
     *         {@code null} otherwise.
     */
    public PropertyDescriptor getPropertyDescriptor()
    {
        return this._propertyDescriptor;
    }

    /**
     * Returns {@link AssociationDescriptor} associated with this association, if this qualified
     * name info represents an association. Returns {@code null} otherwise.
     *
     * @return {@link AssociationDescriptor} if this qualified name info is associated with
     *         association, {@code null} otherwise.
     */
    public AssociationDescriptor getAssociationDescriptor()
    {
        return this._associationDescriptor;
    }

    /**
     * Returns {@link AssociationDescriptor} associated with this many-association, if this
     * qualified name info represents a many-association. Returns {@code null} otherwise.
     *
     * @return {@link AssociationDescriptor} if this qualified name info is associated with
     *         many-association, {@code null} otherwise.
     */
    public AssociationDescriptor getManyAssociationDescriptor()
    {
        return this._manyAssociationDescriptor;
    }

    /**
     * Gets the type of represented qualified name: either {@link QNameType#PROPERTY} for
     * properties, {@link QNameType#ASSOCIATION} for associations, or
     * {@link QNameType#MANY_ASSOCIATION} for many-associations.
     *
     * @return The type of represented qualified name: either {@link QNameType#PROPERTY},
     *         {@link QNameType#ASSOCIATION}, or {@link QNameType#MANY_ASSOCIATION}.
     */
    public QNameType getQNameType()
    {
        return this._qNameType;
    }

    /**
     * Creates information about specified qualified name which represents a property.
     *
     * @param qName The qualified name of property.
     * @param tableName The table name where the values of all instances of propertiy with this
     *        qualified name will be stored. May be {@code null} if it is to be decided later.
     * @param propertyDescriptor {@link PropertyDescriptor} of this property.
     * @return An object representing information about property with this qualified name, and how
     *         instances of this property are stored in database.
     */
    public static QNameInfo fromProperty( //
        QualifiedName qName, //
        String tableName, //
        PropertyDescriptor propertyDescriptor //
    )
    {
        Type vType = propertyDescriptor.type();
        List<Class<?>> collectionClasses = new ArrayList<>();
        while( vType instanceof ParameterizedType
               && Collection.class.isAssignableFrom( (Class<?>) ( (ParameterizedType) vType )
            .getRawType() ) )
        {
            collectionClasses.add( (Class<?>) ( (ParameterizedType) vType ).getRawType() );
            vType = ( (ParameterizedType) vType ).getActualTypeArguments()[0];
        }

        return new QNameInfo( qName, QNameType.PROPERTY, collectionClasses, tableName, vType,
                              propertyDescriptor, null,
                              null );
    }

    /**
     * Creates information about specified qualified name which represents an association.
     *
     * @param qName The qualified name of the association.
     * @param tableName The table name where the values of all instances of association with this
     *        qualified name will be stored. May be {@code null} if it is to be decided later.
     * @param assoDescriptor {@link AssociationDescriptor} of this association.
     * @return An object representing information about association with this qualified name, and
     *         how instances of this association are stored in database.
     */
    public static QNameInfo fromAssociation( //
        QualifiedName qName, //
        String tableName, //
        AssociationDescriptor assoDescriptor //
    )
    {
        return new QNameInfo( qName, QNameType.ASSOCIATION, null, tableName, assoDescriptor.type(),
                              null,
                              assoDescriptor, null );
    }

    /**
     * Creates information about specified qualified name which represents a many-association.
     *
     * @param qName The qualified name of the many-association.
     * @param tableName The table name where the values of all instances of many-association with
     *        this qualified name will be stored. May be {@code null} if it is to be decided later.
     * @param manyAssoDescriptor {@link AssociationDescriptor} of this many-association.
     * @return An object representing information about many-association with this qualified name,
     *         and how instances of this many-association are stored in database.
     */
    public static QNameInfo fromManyAssociation( //
        QualifiedName qName, //
        String tableName, //
        AssociationDescriptor manyAssoDescriptor //
    )
    {
        return new QNameInfo( qName, QNameType.MANY_ASSOCIATION, null, tableName,
                              manyAssoDescriptor.type(), null,
                              null, manyAssoDescriptor );
    }

    @Override
    public String toString()
    {
        return "[table: " + this._tableName + ", final type: " + this._finalType + ", qNameType: "
               + this._qNameType + ", qName: " + this._qName + "]";
    }

}
