/*
 * 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.apache.openjpa.jdbc.identifier;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

import org.apache.openjpa.lib.identifier.Identifier;

/**
 * This class extends DBIdentifier to provide support for qualified identifiers
 * such as schema qualified tables and table qualified columns.  It provides methods
 * to create qualified identifiers from individual identifiers.
 *
 */
public class QualifiedDBIdentifier extends DBIdentifier implements Identifier, Cloneable, Serializable {

    
    private static final long serialVersionUID = 1L;
    private DBIdentifier _schemaName = DBIdentifier.NULL;  // The schema name
    // The table name if the object (column, constraint) is qualified by a table name
    private DBIdentifier _objectTableName = DBIdentifier.NULL;


    protected QualifiedDBIdentifier(DBIdentifier... sNames) {
        setPath(sNames);
    }

    /**
     * Creates a qualified identifier based upon an array of DBIdentifiers.  Identifiers
     * must be specified in order.
     * @param names
     */
    public static QualifiedDBIdentifier newPath(DBIdentifier...names) {
        return new QualifiedDBIdentifier(names);
    }

    /**
     * Set the identifiers that make up the path.  Identifiers must be specified
     * in path order.  (ex. [ table, column ] )
     * @param sNames
     */
    public void setPath(DBIdentifier...sNames) {
        resetNames();
        if (sNames == null || sNames.length == 0) {
            return;
        }

        if (sNames.length == 1) {
            DBIdentifier sName = sNames[0];
            if (sName.getType() == DBIdentifierType.SCHEMA) {
                setSchemaName(sName.clone());
            }
            setName(sName.clone());
            setType(sName.getType());
            return;
        }

        for (int i = (sNames.length - 1); i >= 0; i--) {
            DBIdentifier sName = sNames[i];
            if (DBIdentifier.isNull(sName)) {
                continue;
            }
            if (i == (sNames.length - 1) && sNames.length != 1) {
                setName(sName.clone());
            } else {
                if (sName.getType() == DBIdentifierType.SCHEMA) {
                    setSchemaName(sName.clone());
                }
                else if (sName.getType() == DBIdentifierType.TABLE) {
                    setObjectTableName(sName.clone());
                }
            }
        }
    }

    // Reset the path names
    private void resetNames() {
        _schemaName = DBIdentifier.NULL;
        _objectTableName = DBIdentifier.NULL;
    }

    /**
     * Splits a qualified path into separate identifiers.
     * @param sName
     */
    public static DBIdentifier[] splitPath(DBIdentifier sName) {
        if (sName instanceof QualifiedDBIdentifier && sName.getType() != DBIdentifierType.SCHEMA) {
            QualifiedDBIdentifier path = (QualifiedDBIdentifier)sName;
            List<DBIdentifier> names = new ArrayList<>();

            if (!DBIdentifier.isNull(path.getSchemaName())) {
                names.add(path.getSchemaName().clone());
            }
            if (!DBIdentifier.isNull(path.getObjectTableName())) {
                names.add(path.getObjectTableName().clone());
            }
            if (!DBIdentifier.isNull(path.getIdentifier())) {
                names.add(((DBIdentifier)path).clone());
            }
            return names.toArray(new DBIdentifier[names.size()]);
        }
        if (sName instanceof DBIdentifier) {
            return new DBIdentifier[] { sName.clone() };
        }
        return new DBIdentifier[] {};
    }

    /**
     * Creates a qualified path from an identifier.
     * @param sName
     */
    public static QualifiedDBIdentifier getPath(DBIdentifier sName) {
        if (sName instanceof QualifiedDBIdentifier) {
            return (QualifiedDBIdentifier)sName.clone();
        }
        return QualifiedDBIdentifier.newPath(sName);
    }

    /**
     *Sets the schema component of the path.
     */
    public void setSchemaName(DBIdentifier schemaName) {
        _schemaName = schemaName;
    }

    /**
     * Gets the schema component of the path.
     */
    public DBIdentifier getSchemaName() {
        return _schemaName;
    }

    /**
     * Sets the object table name component of the path, if the path
     * is a table qualified identifier such as a constraint or column.
     */
    public void setObjectTableName(DBIdentifier objectName) {
        _objectTableName = objectName;
    }


    /**
     * Gets the object table name component of the path, if the path
     * is a table qualified identifier such as a constraint or column.
     */
    public DBIdentifier getObjectTableName() {
        return _objectTableName;
    }

    /**
     * Returns true if this object is not qualified by a schema component.
     */
    public boolean isUnqualifiedObject() {
        return DBIdentifier.isNull(getSchemaName());
    }

    /**
     * Returns true if this object is not qualified by a table or schema
     * component.
     */
    public boolean isUnqualifiedColumn() {
        return DBIdentifier.isNull(getObjectTableName()) && DBIdentifier.isNull(getSchemaName());
    }

    /**
     * Equality operator.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (obj instanceof QualifiedDBIdentifier) {
            QualifiedDBIdentifier sPath = (QualifiedDBIdentifier)obj;
            return DBIdentifier.equal(sPath.getSchemaName(), getSchemaName()) &&
                DBIdentifier.equal(sPath.getObjectTableName(), getObjectTableName()) &&
                DBIdentifier.equal(sPath, this);

        }
        else if (obj instanceof DBIdentifier) {
            DBIdentifier sName = (DBIdentifier)obj;
            return DBIdentifier.equal(sName, this);
        } else if (obj instanceof String) {
            return obj.equals(this.getName());
        }
        throw new IllegalArgumentException("Cannot compare to type: " + obj.getClass().getName());
    }

    /**
     * Compares two qualified identifiers for equality.
     * @param path1
     * @param path2
     */
    public static boolean pathEqual(QualifiedDBIdentifier path1, QualifiedDBIdentifier path2) {
        if (path1 == null && path2 == null) {
            return true;
        }
        if (path1 == null) {
            return false;
        }
        DBIdentifier[] names1 = QualifiedDBIdentifier.splitPath(path1);
        DBIdentifier[] names2 = QualifiedDBIdentifier.splitPath(path2);
        if (names1.length != names2.length) {
            return false;
        }
        for (int i = 0; i < names1.length; i++) {
            if (!DBIdentifier.equal(names1[i], names2[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns a fully qualified name as a string.
     */
    @Override
    public String toString() {
        return getName();
    }

    /**
     * Returns the fully qualified name as a string
     */
    @Override
    public String getName() {
        // If no schema or object or table qualifier, return the base name
        if (DBIdentifier.isEmpty(_schemaName) && DBIdentifier.isEmpty(_objectTableName)) {
            return getBaseName();
        }
        DBIdentifier[] names = QualifiedDBIdentifier.splitPath(this);
        return DBIdentifier.join(names);
    }

    /**
     * Returns the base name of this qualified name.  For example, if the
     * qualified name is a schema qualified table, the table name is returned.
     */
    public String getBaseName() {
        return super.getName();
    }

    /**
     * Returns this object as a DBIdentifier.
     */
    public DBIdentifier getIdentifier() {
        return this;
    }

    /**
     * Set the name of this qualified identifier.  Splits the string into
     * multiple components.  This method assumes the type does not change.
     */
    @Override
    public void setName(String name) {
        // Split apart name into components
        DBIdentifier[] sNames = DBIdentifier.split(getType(), name);
        setPath(sNames);
    }

    /**
     * Set the base name component of this compound identifier
     * @param name
     */
    protected void setBaseName(String name) {
        super.setName(name);
    }

    /**
     * Returns the length of the qualified identifier, including delimiters
     * and name separators.
     */
    @Override
    public int length() {
        String name = getName();
        if (name == null) {
            return 0;
        }
        return name.length();
    }

    /**
     * Compares this identifier with another identifier.
     */
    @Override
    public int compareTo(Identifier o) {
        if (o instanceof DBIdentifier) {
            if (o == null || o == DBIdentifier.NULL)
                return -1;
            return super.compareTo(o);
        }
        return getName().compareTo(o.getName());
    }

    /**
     * Returns true if all identifiers within this compound identifier are
     * delimited. Otherwise, false.
     */
    @Override
    public boolean isDelimited() {
        if (DBIdentifier.isEmpty(this)) {
            return false;
        }
        if (!DBIdentifier.isNull(getObjectTableName())) {
            if (!Normalizer.isDelimited(getObjectTableName().getName())) {
                return false;
            }
        }
        if (!DBIdentifier.isNull(getSchemaName())) {
            if (!Normalizer.isDelimited(getSchemaName().getName())) {
                return false;
            }
        }
        return super.isDelimited();
    }

    /**
     * Returns a new unqualified name based on this instance.
     */
    @Override
    public DBIdentifier getUnqualifiedName() {
        QualifiedDBIdentifier newName = (QualifiedDBIdentifier)clone();
        newName.setObjectTableName(DBIdentifier.NULL);
        newName.setSchemaName(DBIdentifier.NULL);
        return newName;
    }

    /**
     * Creates a clone of this identifier.
     */
    @Override
    public QualifiedDBIdentifier clone() {
        QualifiedDBIdentifier sPath = new QualifiedDBIdentifier();
        sPath.setObjectTableName(getObjectTableName().clone());
        sPath.setSchemaName(getSchemaName().clone());
        sPath.setBaseName(super.getName());
        sPath.setType(getType());
        sPath.setIgnoreCase(getIgnoreCase());
        return sPath;
    }

}
