/*
 * 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.netbeans.modules.dbschema;

import java.util.ArrayList;
import java.util.ResourceBundle;
import java.text.MessageFormat;

/** Describes an entire database table.
 */
public final class TableElement extends DBElement implements ColumnElementHolder, ColumnPairElementHolder {
	/** Constant indicating that the table is a real table.
	 * @see #isTableOrView
	 */
	public static final boolean TABLE = true;

	/** Constant indicating that the table is a view.
	 * @see #isTableOrView
	 */
	public static final boolean VIEW = false;

	/** the schema to which this table element belongs */
	private transient SchemaElement declaringSchema;

    /** Creates a new table element represented in memory.
     */
	public TableElement() {
        this(new Memory(), null);
	}

	/** Createa a table element.
	 * @param impl the pluggable implementation
	 * @param declaringSchema the schema to which this element belongs
	 */
	public TableElement(Impl impl, SchemaElement declaringSchema) {
		super(impl);
		this.declaringSchema = declaringSchema;
	}

    /** Returns the implementation for the table.
	 * @return implementation for the table
	 */
	final Impl getTableImpl() {
        return (Impl) getElementImpl();
    }

    /** Returns the TableElement object associated with the given name and with the given schema.
     * @param name the table name
     * @param schema the schema
     * @return the TableElement object for the given table name in the given schema
     */
	public static TableElement forName(String name, SchemaElement schema) {
        int pos = name.lastIndexOf(".");
        if (pos == -1)
            return null;
        else
            name = name.substring(pos + 1);
        
        TableElement[] tes = schema.getTables();
        for (int i = 0; i < tes.length; i++)
            if (tes[i].getName().getName().trim().equals(name))
                return tes[i];

        return null;
	}

    /** Returns the TableElement object associated with the given name.
     * @param name the table name
     * @return the TableElement object for the given table name
     */
	public static TableElement forName(String name) /* 4_ea throws DBException */ {
        int index = name.lastIndexOf("."); //NOI18N
        
        if (index == -1) {
            if (Boolean.getBoolean("netbeans.debug.exceptions")) // NOI18N
                System.out.println(ResourceBundle.getBundle("org.netbeans.modules.dbschema.resources.Bundle").getString("FullyQualifiedName")); //NOI18N
            return null;
        }
        
        SchemaElement se = SchemaElement.forName(name.substring(0, index));
        
        if (se == null)
// 4_ea            throw new DBException(bundle.getString("SchemaNotFound"));
            return null;
        else
            return TableElement.forName(name, se);
	}

	/** Sets whether this is really a table, or a view.
	 * @param isTable one of {@link #TABLE} or {@link #VIEW}
	 * @throws DBException if impossible
	 */
	public void setTableOrView(boolean isTable) throws DBException {
		    getTableImpl().setTableOrView(isTable);
	}

	/** Tests whether this is a table, or a view.
	 * @return one of {@link #TABLE} or {@link #VIEW}
	 */
	public boolean isTableOrView() {
            return getTableImpl().isTableOrView();
  }

	/** Tests whether this is really a table.
	 * @return <code>true</code> if so
	 * @see #isTableOrView
	 */
	public boolean isTable() { 
            return getTableImpl().isTableOrView();
  }

	/** Test whether this is a view.
	 * @return <code>true</code> if so
	 * @see #isTableOrView
	 */
	public boolean isView() {
            return !getTableImpl().isTableOrView();
  }

	// ==================== schema section ==========================

	/* This should be automatically synchronized
	* when a TableElement is added to the schema. */
  
	/** Gets the declaring schema. 
	 * @return the schema that owns this table element
	 */
	public final SchemaElement getDeclaringSchema() {
        return declaringSchema;
    }

	/** Sets the declaring schema. 
	 * @param se the schema that owns this table element
	 */
	public final void setDeclaringSchema(SchemaElement se) {
        if (declaringSchema == null)
            declaringSchema = se;
    }

	//================== Columns ===============================

	/** Adds a new column to the table.
	 * @param el the column to add
	 * @throws DBException if impossible
	 */
	public void addColumn(ColumnElement el) throws DBException {
		addColumns(new ColumnElement[] {el});
	}

	/** Adds some new columns to the table.
	 * @param els the columns to add
	 * @throws DBException if impossible
	 */
	public void addColumns(final ColumnElement[] els) throws DBException {
		for (int i = 0; i < els.length; i++) {
			if (getColumn(els[i].getName()) != null)
				throwAddException("FMT_EXC_AddColumn", els[i]); //NOI18N
            if (els[i].getDeclaringTable() == null)
                els[i].setDeclaringTable(this);
        }
        getTableImpl().changeColumns(els, Impl.ADD);
	}

	/** Removes a column from the table.
	 * @param el the column to remove
	 * @throws DBException if impossible
	 */
	public void removeColumn(ColumnElement el) throws DBException {
		removeColumns(new ColumnElement[] {el});
	}

	/** Removes some columns from the table.
	 * @param els the columns to remove
	 * @throws DBException if impossible
	 */
	public void removeColumns(final ColumnElement[] els) throws DBException {
    		getTableImpl().changeColumns(els, Impl.REMOVE);
	}

	/** Sets the columns for this table.
	 * Previous columns are removed.
	 * @param els the new columns
	 * @throws DBException if impossible
	 */
	public void setColumns(ColumnElement[] els) throws DBException {
            getTableImpl().changeColumns(els, Impl.SET);
	}

	/** Gets all columns in this table.
	 * @return the columns
	 */
	public ColumnElement[] getColumns() {
            return getTableImpl().getColumns();
    }

	/** Finds a column by name.
	 * @param name the name of the column for which to look
	 * @return the element or <code>null</code> if not found
	 */
	public ColumnElement getColumn(DBIdentifier name) {
    		return getTableImpl().getColumn(name);
	}

	//================== Indexes ===============================

	/** Adds a new index to the table.
	 * @param el the index to add
	 * @throws DBException if impossible
	 */
	public void addIndex(IndexElement el) throws DBException {
		addIndexes(new IndexElement[] {el});
	}

	/** Adds some new indexes to the table.
	 * @param els the indexes to add
	 * @throws DBException if impossible
	 */
	public void addIndexes(final IndexElement[] els) throws DBException {
		for (int i = 0; i < els.length; i++) {
			if (getIndex(els[i].getName()) != null)
				throwAddException("FMT_EXC_AddColumn", els[i]); //NOI18N
            if (els[i].getDeclaringTable() == null)
                els[i].setDeclaringTable(this);
        }

        getTableImpl().changeIndexes(els, Impl.ADD);
	}

	/** Removesan index from the table.
	 * @param el the index to remove
	 * @throws DBException if impossible
	 */
	public void removeIndex(IndexElement el) throws DBException {
		removeIndexes(new IndexElement[] {el});
	}

	/** Removes some indexes from the table.
	 * @param els the indexes to remove
	 * @throws DBException if impossible
	 */
	public void removeIndexes(final IndexElement[] els) throws DBException {
        getTableImpl().changeIndexes(els, Impl.REMOVE);
	}

	/** Sets the indexes for this table.
	 * Previous indexes are removed.
	 * @param els the new indexes
	 * @throws DBException if impossible
	 */
	public void setIndexes(IndexElement[] els) throws DBException {
        getTableImpl().changeIndexes(els, Impl.SET);
	}

	/** Gets all indexes in this table.
	 * @return the indexes
	 */
	public IndexElement[] getIndexes() {
        return getTableImpl().getIndexes();
    }

	/** Finds an index by name.
	 * @param name the name of the index for which to look
	 * @return the element or <code>null</code> if not found
	 */
	public IndexElement getIndex(DBIdentifier name) {
        return getTableImpl().getIndex(name);
	}

	//================== Keys (Unique, Primary, and Foreign) =================

	/** Adds a new key to the table.
	 *  @param el the key to add
	 * @throws DBException if impossible
	 */
	public void addKey(KeyElement el) throws DBException {
		addKeys(new KeyElement[]{el});
	}

	/** Adds some new keys to the table.
	 *  @param els the keys to add
	 * @throws DBException if impossible
	 */
	public void addKeys(final KeyElement[] els) throws DBException {
		for (int i = 0; i < els.length; i++) {
			if (getKey(els[i].getName()) != null)
				throwAddException("FMT_EXC_AddColumn", els[i]); //NOI18N
            if (els[i].getDeclaringTable() == null)
                els[i].setDeclaringTable(this);
            if (els[i] instanceof UniqueKeyElement)
                if (((UniqueKeyElement) els[i]).getAssociatedIndex() == null) {
                    IndexElement ie  = new IndexElement();
                    try {
                        ie.setName(els[i].getName());
                        ie.setColumns(els[i].getColumns());
                        this.addIndex(ie);
                    } catch (DBException exc) {
                        exc.printStackTrace();
                    }
                    ((UniqueKeyElement) els[i]).setAssociatedIndex(ie);
                }
        }
	
        getTableImpl().changeKeys(els, Impl.ADD);
	}

	/** Removes a key from the table.
	 *  @param el the key to remove
	 * @throws DBException if impossible
	 */
	public void removeKey(KeyElement el) throws DBException {
		removeKeys(new KeyElement[] {el});
	}

	/** Removes some keys from the table.
	 *  @param els the keys to remove
	 * @throws DBException if impossible
	 */
	public void removeKeys(final KeyElement[] els) throws DBException {
    		getTableImpl().changeKeys(els, Impl.REMOVE);
	}

	/** Sets the keys for this table.
	 * Previous keys are removed.
	 * @param els the new keys
	 * @throws DBException if impossible
	 */

	public void setKeys(KeyElement[] els) throws DBException {
            getTableImpl().changeKeys(els, Impl.SET);
	}

	/** Gets all keys in this table.
	 * @return the keys
	 */
	public KeyElement[] getKeys() {
    		return getTableImpl().getKeys();
	}

	/** Finds a key by name.
	 * @param name the name of the key for which to look
	 * @return the element or <code>null</code> if not found
	 */
	public KeyElement getKey(DBIdentifier name) {
    		return getTableImpl().getKey(name);
	}

	// key convenience methods

	/** Gets all keys in this table of the given subtype.
     * @param subtype the type of the key classes
	 * @return the keys of the given subtype or <code>null</code> if not found
	 */
	private ArrayList getKeys(Class subtype) {
		KeyElement[] keys = getKeys();

        if (keys == null)
            return null;

		int i, count = keys.length;
		ArrayList subKeys = new ArrayList(count);

		for (i = 0; i < count; i++) {
			KeyElement key = keys[i];

			if (subtype.isInstance(key))
				subKeys.add(key);
		}

		return subKeys;
	}

	/** Gets all foreign keys in this table.
	 * @return the foreign keys or <code>null</code> if not found
	 */
	public ForeignKeyElement[] getForeignKeys() {
		ArrayList keys = getKeys(ForeignKeyElement.class);
        
        if (keys == null)
            return null;

		int count = keys.size();

		return ((ForeignKeyElement[]) keys.toArray(new ForeignKeyElement[count]));
	}

	/** Finds a foreign key by name.
	 * @param name the name of the foreign key for which to look
	 * @return the foreign key or <code>null</code> if not found
	 */
	public ForeignKeyElement getForeignKey(DBIdentifier name) {
		ForeignKeyElement[] fks = getForeignKeys();
		int i, count = fks.length;
		
		for (i = 0; i < count; i++) {
			ForeignKeyElement fk = fks[i];

			if (name.equals(fk.getName()))
				return fk;
		}

		return null;
	}
	
	/** Gets all unique keys in this table.
	 * @return the unique keys or <code>null</code> if not found
	 */
	public UniqueKeyElement[] getUniqueKeys() {
		ArrayList keys = getKeys(UniqueKeyElement.class);

		if (keys == null)
            return null;

		return ((UniqueKeyElement[]) keys.toArray(new UniqueKeyElement[keys.size()]));
	}

	/** Finds a unique key by name.
	 * @param name the name of the unique key for which to look
	 * @return the unique key or <code>null</code> if not found
	 */
	public UniqueKeyElement getUniqueKey(DBIdentifier name) {
		UniqueKeyElement[] uks = getUniqueKeys();
		int i, count = uks.length;
		
		for (i = 0; i < count; i++) {
			UniqueKeyElement uk = uks[i];

			if (name.equals(uk.getName()))
				return uk;
		}

		return null;
	}

	/** Finds the primary key.
	 * @return the primary key or <code>null</code> if not found
	 */
	public UniqueKeyElement getPrimaryKey() {
		UniqueKeyElement[] uks = getUniqueKeys();

        if (uks == null)
            return null;

        for (int i = 0; i < uks.length; i++) {
            UniqueKeyElement uk = uks[i];

            if (uk.isPrimaryKey())
                return uk;
        }

		return null;
	}

	// end key convenience methods

    /** This method just throws localized exception. It is used during
     * adding class element, which already exists in source.
     * @param formatKey The message format key to localized bundle.
     * @param element The element which can't be added
     * @exception DBException is alway thrown from this method.
     */
    private void throwAddException(String formatKey, DBMemberElement element) throws DBException {
        // MessageFormat format = new MessageFormat(ElementFormat.bundle.getString(formatKey));
        String msg = /*format.format(new Object[] { */element.getName().getName();// });
        throw new DBException(msg);
    }
    
    /** Returns a string representation of the object.
     * @return a string representation of the object.
     */
    public String toString() {
        if (getName() != null)
            return getName().toString();
        else
            return null;
    }

    /** Adds a new column pair to the holder.
     * @param pair the pair to add
     * @throws DBException if impossible
     */
    public void addColumnPair(ColumnPairElement pair) throws DBException {
		addColumnPairs(new ColumnPairElement[] {pair});
    }    

    /** Adds some new column pairs to the holder.
     * @param pairs the column pairs to add
     * @throws DBException if impossible
     */
    public void addColumnPairs(ColumnPairElement[] pairs) throws DBException {
		for (int i = 0; i < pairs.length; i++) {
			if (getColumnPair(pairs[i].getName()) != null)
				throwAddException("FMT_EXC_AddColumn", pairs[i]); //NOI18N
            if (pairs[i].getDeclaringTable() == null)
                pairs[i].setDeclaringTable(this);
        }
        getTableImpl().changeColumnPairs(pairs, Impl.ADD);
    }
    
    /** Removes a column pair from the holder.
     * @param pair the column pair to remove
     * @throws DBException if impossible
     */
    public void removeColumnPair(ColumnPairElement pair) throws DBException {
		removeColumnPairs(new ColumnPairElement[] {pair});
    }
    
    /** Removes some column pairs from the holder.
     * @param pairs the column pairs to remove
     * @throws DBException if impossible
     */
    public void removeColumnPairs(ColumnPairElement[] pairs) throws DBException {
        getTableImpl().changeColumnPairs(pairs, Impl.REMOVE);
    }
    
    /** Sets the column pairs for this holder.
     * Previous column pairs are removed.
     * @param pairs the new column pairs
     * @throws DBException if impossible
     */
    public void setColumnPairs(ColumnPairElement[] pairs) throws DBException {
        getTableImpl().changeColumnPairs(pairs, Impl.SET);
    }
    
    /** Gets all column pairs in this holder.
     * @return the column pairs
     */
    public ColumnPairElement[] getColumnPairs() {
        return getTableImpl().getColumnPairs();
    }
    
    /** Finds a column pair by name.
     * @param name the name of the column pair for which to look
     * @return the column pair or <code>null</code> if not found
     */
    public ColumnPairElement getColumnPair(DBIdentifier name) {
        return getTableImpl().getColumnPair(name);
    }

    /** Finds a database member element by name.
     * @param name the name of the database member element for which to look
     * @return the database member element or <code>null</code> if not found
     */
    public DBMemberElement getMember(DBIdentifier name) {
        int index = name.getName().indexOf(";"); //NOI18N
        
        if (index == -1)
            //column
            return getColumn(name);
        else
            //column pair
            return getColumnPair(name);
    }
    
	/** Pluggable behaviour for table elements.
	 * @see TableElement
	 */
	public static interface Impl extends DBElement.Impl {
		/** Sets whether this is really a table, or a view.
		 * @param isTable one of {@link #TABLE} or {@link #VIEW}
		 * @throws DBException if impossible
		 */
		public void setTableOrView(boolean isTable) throws DBException;

		/** Tests whether this is a table, or a view.
		 * @return one of {@link #TABLE} or {@link #VIEW}
		 */
		public boolean isTableOrView();

		/** Changes the set of columns.
		* @param elems the columns to change
		* @param action one of {@link #ADD}, {@link #REMOVE}, or {@link #SET}
		* @exception DBException if the action cannot be handled
		*/
		public void changeColumns(ColumnElement[] elems, int action) throws DBException;

		/** Gets all columns.
		* @return the columns
		*/
		public ColumnElement[] getColumns();

		/** Finds a column by name.
		* @param name the name for which to look
		* @return the column, or <code>null</code> if it does not exist
		*/
		public ColumnElement getColumn(DBIdentifier name);

		/** Changes the set of indexes.
		* @param elems the indexes to change
		* @param action one of {@link #ADD}, {@link #REMOVE}, or {@link #SET}
		* @exception DBException if the action cannot be handled
		*/
		public void changeIndexes(IndexElement[] elems, int action) throws DBException;

		/** Gets all indexes.
		* @return the indexes
		*/
		public IndexElement[] getIndexes();

		/** Finds an index by name.
		* @param name the name for which to look
		* @return the index, or <code>null</code> if it does not exist
		*/
		public IndexElement getIndex(DBIdentifier name);

		/** Changes the set of keys.
		* @param elems the keys to change
		* @param action one of {@link #ADD}, {@link #REMOVE}, or {@link #SET}
		* @exception DBException if the action cannot be handled
		*/
		public void changeKeys(KeyElement[] elems, int action) throws DBException;

		/** Gets all keys.
		* @return the keys
		*/
		public KeyElement[] getKeys();

		/** Finds a key by name.
		* @param name the name for which to look
		* @return the key, or <code>null</code> if it does not exist
		*/
		public KeyElement getKey(DBIdentifier name);
        
		/** Changes the set of column pairs.
		* @param pairs the column pairs to change
		* @param action one of {@link #ADD}, {@link #REMOVE}, or {@link #SET}
		* @exception DBException if the action cannot be handled
		*/
		public void changeColumnPairs(ColumnPairElement[] pairs, int action) throws DBException;
        
		/** Gets all column pairs.
		* @return the column pairs
		*/
        public ColumnPairElement[] getColumnPairs();

		/** Finds a column pair by name.
		* @param name the name for which to look
		* @return the column pair, or <code>null</code> if it does not exist
		*/
        public ColumnPairElement getColumnPair(DBIdentifier name);
	}

	/** Memory based implementation of the element factory.
	 */
	static final class Memory extends DBElement.Memory implements Impl {
		/** is table or view */
		private boolean _isTable;

		/** collection of columns */
		private DBMemoryCollection.Column columns;

		/** collection of indexes */
		private DBMemoryCollection.Index indexes;

		/** collection of keys */
		private DBMemoryCollection.Key keys;
        
		/** collection of column pairs */
		private DBMemoryCollection.ColumnPair pairs;

        /** Default constructor.
         */
		public Memory() {
            super();
			_isTable = true;
		}

		/** Copy constructor.
		 * @param el element to copy from
		 */
		public Memory(TableElement el) {
			super(el);
			_isTable = el.isTableOrView();
		}

		/** Sets whether this is really a table, or a view.
		 * @param isTable one of {@link #TABLE} or {@link #VIEW}
		 * @throws DBException if impossible
		 */
		public void setTableOrView(boolean isTable) {
			boolean old = _isTable;

			_isTable = isTable;
			firePropertyChange(PROP_TABLE_OR_VIEW, Boolean.valueOf(old), Boolean.valueOf(isTable));
		}

		/** Tests whether this is a table, or a view.
		 * @return one of {@link #TABLE} or {@link #VIEW}
		 */
        public boolean isTableOrView() {
            return _isTable;
        }

		/** Changes set of columns.
		 * @param elems elements to change
         * @param action the action to do
		 * @exception SourceException if the action cannot be handled
		 */
		public synchronized void changeColumns(ColumnElement[] elems, int action) throws DBException {
			initColumns();
			columns.change(elems, action);
		}

        /** Gets all columns
         * @return the columns
         */
        public synchronized ColumnElement[] getColumns() {
			initColumns();
			return (ColumnElement[]) columns.getElements();
        }

		/** Finds a column with given name.
		 * @param name the name of column for which to look
		 * @return the element or null if column with such name does not exist
		 */
        public synchronized ColumnElement getColumn(DBIdentifier name) {
			initColumns();
			return (ColumnElement) columns.getElement(name);
        }

        /** Initializes the collection of columns.
         */
		void initColumns() {
			if (columns == null)
				columns = new DBMemoryCollection.Column(this);
		}

		/** Changes set of indexes.
		 * @param elems elements to change
         * @param action the action to do
		 * @exception SourceException if the action cannot be handled
		 */
		public synchronized void changeIndexes(IndexElement[] elems, int action) throws DBException  {
			initIndexes();
			indexes.change(elems, action);
		}

        /** Gets all indexes.
         * @return the indexes
         */
        public synchronized IndexElement[] getIndexes() {
			initIndexes();
			return (IndexElement[]) indexes.getElements();
        }

		/** Finds an index with given name.
		 * @param name the name of index for which to look
		 * @return the element or null if index with such name does not exist
		 */
        public synchronized IndexElement getIndex(DBIdentifier name) {
			initIndexes();
			return (IndexElement) indexes.getElement(name);
        }

        /** Initializes the collection of indexes.
         */
		void initIndexes() {
			if (indexes == null)
				indexes = new DBMemoryCollection.Index(this);
		}
		
		/** Changes set of keys.
		 * @param elems elements to change
         * @param action the action to do
		 * @exception SourceException if the action cannot be handled
		 */
		public synchronized void changeKeys(KeyElement[] elems, int action) throws DBException {
			initKeys();
			keys.change(elems, action);
		}

        /** Gets all keys.
         * @return the keys
         */
        public synchronized KeyElement[] getKeys() {
			initKeys();
			return (KeyElement[]) keys.getElements();
        }

		/** Finds a key with given name.
		 * @param name the name of key for which to look
		 * @return the element or null if key with such name does not exist
		 */
        public synchronized KeyElement getKey(DBIdentifier name) {
			initKeys();
			return (KeyElement) keys.getElement(name);
        }

        /** Initializes the collection of keys.
         */
		void initKeys() {
			if (keys == null)
				keys = new DBMemoryCollection.Key(this);
		}

		/** Getter for the associated table
		 * @return the table element for this impl
		 */
		final TableElement getTableElement() {
			return (TableElement) _element;
		}
        
        /** Gets all column pairs.
         * @return the column pairs
         */
        public synchronized ColumnPairElement[] getColumnPairs() {
			initColumnPairs();
			return (ColumnPairElement[]) pairs.getElements();
        }
        
		/** Finds a column pair with given name.
		 * @param name the name of column pair for which to look
		 * @return the column pair or null if key with such name does not exist
		 */
        public synchronized ColumnPairElement getColumnPair(DBIdentifier name) {
			initColumnPairs();
			return (ColumnPairElement) pairs.getElement(name);
        }
        
		/** Changes set of column pairs.
		 * @param pairs elements to change
         * @param action the action to do
		 * @exception SourceException if the action cannot be handled
		 */
        public synchronized void changeColumnPairs(ColumnPairElement[] pairs, int action) throws DBException {
			initColumnPairs();
			this.pairs.change(pairs, action);
        }
        
        /** Initializes the collection of column pairs.
         */
		void initColumnPairs() {
			if (pairs == null)
				pairs = new DBMemoryCollection.ColumnPair(this);
		}
	}
}
