/*
 * Copyright (C) 2010 eXo Platform SAS.
 * 
 * This is free software; you can redistribute it and/or modify it under the
 * terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This software is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA, or see the FSF
 * site: http://www.fsf.org.
 */
package org.xcmis.search.content;

import org.apache.commons.lang.Validate;
import org.xcmis.search.model.Query;
import org.xcmis.search.model.constraint.Operator;
import org.xcmis.search.model.source.SelectorName;
import org.xcmis.search.value.PropertyType;

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

/**
 * In memory {@link Schema} implementation.
 */
public class InMemorySchema implements Schema
{
   /**
    * Obtain a new instance for building Schema objects.
    * 
    * @return the new builder; never null
    */
   public static Builder createBuilder()
   {
      return new Builder();
   }

   /**
    * A builder of immutable {@link Schema} objects.
    */

   public static class Builder
   {

      private final Map<SelectorName, InMemoryTable> tables = new HashMap<SelectorName, InMemoryTable>();

      private final Map<SelectorName, Query> viewDefinitions = new HashMap<SelectorName, Query>();

      protected Builder()
      {
      }

      /**
       * Add a table with the supplied name and column names. Each column will
       * be given a default type. The table will also overwrite any existing
       * table definition with the same name.
       * 
       * @param name
       *           the name of the new table
       * @param columnNames
       *           the names of the columns.
       * @return this builder, for convenience in method chaining; never null
       */
      public Builder addTable(String name, String... columnNames)
      {
         Validate.notEmpty(name, " name may not be empty");
         Validate.notEmpty(columnNames, "columnNames may not be empty");
         List<Column> columns = new ArrayList<Column>();
         int i = 0;
         for (String columnName : columnNames)
         {
            Validate.notEmpty(columnName, "columnName[" + (i++) + "] may not be empty");;
            // TODO default type
            columns.add(new InMemoryColumn(columnName, PropertyType.STRING));
         }
         InMemoryTable table = new InMemoryTable(new SelectorName(name), columns);
         tables.put(table.getName(), table);
         return this;
      }

      /**
       * Add a table with the supplied name and column names and types. The
       * table will also overwrite any existing table definition with the same
       * name.
       * 
       * @param name
       *           the name of the new table
       * @param columnNames
       *           the names of the columns
       * @param types
       *           the types for the columns
       * @return this builder, for convenience in method chaining; never null
       */
      public Builder addTable(String name, String[] columnNames, PropertyType[] types)
      {
         Validate.notEmpty(name, " name may not be empty");
         Validate.notEmpty(columnNames, " columnNames may not be empty");
         Validate.notEmpty(types, " types may not be empty");
         Validate.isTrue(columnNames.length == types.length, "columnNames.length should be equal types.length");
         List<Column> columns = new ArrayList<Column>();
         assert columnNames.length == types.length;
         for (int i = 0; i != columnNames.length; ++i)
         {
            String columnName = columnNames[i];
            Validate.notEmpty(columnName, " columnName[" + i + "] may not be empty");
            columns.add(new InMemoryColumn(columnName, types[i]));
         }
         InMemoryTable table = new InMemoryTable(new SelectorName(name), columns);
         tables.put(table.getName(), table);
         return this;
      }

      /**
       * Add a column with the supplied name and type to the named table. Any
       * existing column with that name will be replaced with the new column. If
       * the table does not yet exist, it will be added.
       * 
       * @param tableName
       *           the name of the new table
       * @param columnName
       *           the names of the column
       * @param type
       *           the type for the column
       * @return this builder, for convenience in method chaining; never null
       */
      public Builder addColumn(String tableName, String columnName, PropertyType type)
      {
         Validate.notEmpty(tableName, " tableName may not be empty");
         Validate.notEmpty(columnName, " columnName may not be empty");
         Validate.notNull(type, " type may not be null");

         return addColumn(tableName, columnName, type, InMemoryColumn.DEFAULT_FULL_TEXT_SEARCHABLE, Operator.ALL);
      }

      /**
       * Add a column with the supplied name and type to the named table. Any
       * existing column with that name will be replaced with the new column. If
       * the table does not yet exist, it will be added.
       * 
       * @param tableName
       *           the name of the new table
       * @param columnName
       *           the names of the column
       * @param type
       *           the type for the column
       * @param fullTextSearchable
       *           true if the column should be full-text searchable, or false
       *           if not
       * @return this builder, for convenience in method chaining; never null
       */
      public Builder addColumn(String tableName, String columnName, PropertyType type, boolean fullTextSearchable,
         Operator[] availableQueryOperators)
      {
         Validate.notEmpty(tableName, " tableName may not be empty");
         Validate.notEmpty(columnName, " columnName may not be empty");
         Validate.notNull(type, " type may not be null");
         SelectorName selector = new SelectorName(tableName);
         InMemoryTable existing = tables.get(selector);
         InMemoryTable table = null;
         if (existing == null)
         {
            List<Column> columns = new ArrayList<Column>();
            columns.add(new InMemoryColumn(columnName, type, fullTextSearchable, availableQueryOperators));
            table = new InMemoryTable(selector, columns);
         }
         else
         {
            table = existing.withColumn(columnName, type, fullTextSearchable, availableQueryOperators);
         }
         tables.put(table.getName(), table);
         return this;
      }

      /**
       * Make sure the column on the named table is searchable.
       * 
       * @param tableName
       *           the name of the new table
       * @param columnName
       *           the names of the column
       * @return this builder, for convenience in method chaining; never null
       */
      public Builder makeSearchable(String tableName, String columnName)
      {
         Validate.notEmpty(tableName, " tableName may not be empty");
         Validate.notEmpty(columnName, " columnName may not be empty");
         SelectorName selector = new SelectorName(tableName);
         InMemoryTable existing = tables.get(selector);
         InMemoryTable table = null;
         if (existing == null)
         {
            List<Column> columns = new ArrayList<Column>();
            // TODO default type
            columns.add(new InMemoryColumn(columnName, PropertyType.STRING, true, Operator.ALL));
            table = new InMemoryTable(selector, columns);
         }
         else
         {
            Column column = existing.getColumn(columnName);
            // TODO default type
            PropertyType type = PropertyType.STRING;
            if (column != null)
            {
               type = column.getPropertyType();
            }
            table = existing.withColumn(columnName, type, true, column.getAvailableQueryOperators());
         }
         tables.put(table.getName(), table);
         return this;
      }

      /**
       * Build the {@link Schema} instance, using the current state of the
       * builder. This method creates a snapshot of the tables (with their
       * columns) as they exist at the moment this method is called.
       * 
       * @return the new Schema; never null
       */
      public Schema build()
      {
         InMemorySchema schemata = new InMemorySchema(new HashMap<SelectorName, Table>(tables));

         return schemata;
      }
   }

   private final Map<SelectorName, Table> tables;

   protected InMemorySchema(Map<SelectorName, Table> tables)
   {
      this.tables = Collections.unmodifiableMap(tables);
   }

   /**
    * {@inheritDoc}
    * 
    * @see org.modeshape.graph.query.validate.Schema#getTable(org.modeshape.graph.query.model.SelectorName)
    */
   public Table getTable(SelectorName name)
   {
      return tables.get(name);
   }

   public InMemorySchema with(Table table)
   {
      Map<SelectorName, Table> tables = new HashMap<SelectorName, Table>(this.tables);
      tables.put(table.getName(), table);
      return new InMemorySchema(tables);
   }

   /**
    * {@inheritDoc}
    * 
    * @see java.lang.Object#toString()
    */
   @Override
   public String toString()
   {
      StringBuilder sb = new StringBuilder();
      boolean first = true;
      for (Table table : tables.values())
      {
         if (first)
         {
            first = false;
         }
         else
         {
            sb.append('\n');
         }
         sb.append(table);
      }
      return sb.toString();
   }

}
