/*
 * Copyright © 2021-present Arcade Data Ltd (info@arcadedata.com)
 *
 * 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.
 *
 * SPDX-FileCopyrightText: 2021-present Arcade Data Ltd (info@arcadedata.com)
 * SPDX-License-Identifier: Apache-2.0
 */
/* Generated By:JJTree: Do not edit this line. OCreateIndexStatement.java Version 4.3 */
/* JavaCCOptions:MULTI=true,NODE_USES_PARSER=false,VISITOR=true,TRACK_TOKENS=true,NODE_PREFIX=O,NODE_EXTENDS=,NODE_FACTORY=,SUPPORT_USERTYPE_VISIBILITY_PUBLIC=true */
package com.arcadedb.query.sql.parser;

import com.arcadedb.database.Database;
import com.arcadedb.exception.CommandExecutionException;
import com.arcadedb.exception.CommandSQLParsingException;
import com.arcadedb.index.lsm.LSMTreeIndexAbstract;
import com.arcadedb.query.sql.executor.CommandContext;
import com.arcadedb.query.sql.executor.InternalResultSet;
import com.arcadedb.query.sql.executor.ResultInternal;
import com.arcadedb.query.sql.executor.ResultSet;
import com.arcadedb.schema.EmbeddedSchema;
import com.arcadedb.schema.Schema;

import java.util.*;
import java.util.concurrent.atomic.*;
import java.util.stream.*;

public class CreateIndexStatement extends DDLStatement {

  protected Identifier                         name;
  protected Identifier                         typeName;
  protected List<Property>                     propertyList = new ArrayList<Property>();
  protected Identifier                         type;
  protected boolean                            ifNotExists  = false;
  protected Identifier                         engine;
  protected Json                               metadata;
  protected LSMTreeIndexAbstract.NULL_STRATEGY nullStrategy = LSMTreeIndexAbstract.NULL_STRATEGY.SKIP;
  protected List<Identifier>                   keyTypes     = new ArrayList<Identifier>();

  public CreateIndexStatement(final int id) {
    super(id);
  }

  @Override
  public void validate() throws CommandSQLParsingException {
    final String typeAsString = type.getStringValue();
    if (typeAsString.equalsIgnoreCase("FULL_TEXT"))
      ;
    else if (typeAsString.equalsIgnoreCase("UNIQUE"))
      ;
    else if (typeAsString.equalsIgnoreCase("NOTUNIQUE"))
      ;
    else
      throw new CommandSQLParsingException("Index type '" + typeAsString + "' is not supported");
  }

  @Override
  public ResultSet executeDDL(final CommandContext context) {
    final Database database = context.getDatabase();

    if (name == null)
      // GENERATE THE NAME AUTOMATICALLY
      name = new Identifier(typeName.getStringValue() + propertyList.toString().replace(", ", ","));

    if (database.getSchema().existsIndex(name.getValue())) {
      if (ifNotExists) {
        return null;
      } else {
        throw new CommandExecutionException("Index '" + name + "' already exists");
      }
    }

    final String[] fields = calculateProperties(context);

    final EmbeddedSchema.INDEX_TYPE indexType;
    boolean unique = false;

    final String typeAsString = type.getStringValue();
    if (typeAsString.equalsIgnoreCase("FULL_TEXT"))
      indexType = Schema.INDEX_TYPE.FULL_TEXT;
    else if (typeAsString.equalsIgnoreCase("UNIQUE")) {
      indexType = Schema.INDEX_TYPE.LSM_TREE;
      unique = true;
    } else if (typeAsString.equalsIgnoreCase("NOTUNIQUE")) {
      indexType = Schema.INDEX_TYPE.LSM_TREE;
    } else if (typeAsString.equalsIgnoreCase("HSNW")) {
      indexType = Schema.INDEX_TYPE.HSNW;
      unique = true;
    } else
      throw new CommandSQLParsingException("Index type '" + typeAsString + "' is not supported");

    final AtomicLong total = new AtomicLong();

    database.getSchema().buildTypeIndex(typeName.getStringValue(), fields).withType(indexType).withUnique(unique)
        .withPageSize(LSMTreeIndexAbstract.DEF_PAGE_SIZE).withNullStrategy(nullStrategy).withCallback((document, totalIndexed) -> {
          total.incrementAndGet();

          if (totalIndexed % 100000 == 0) {
            System.out.print(".");
            System.out.flush();
          }
        }).create();

    final InternalResultSet rs = new InternalResultSet();
    final ResultInternal result = new ResultInternal();
    result.setProperty("operation", "create index");
    result.setProperty("name", name.getValue());
    result.setProperty("type", indexType);
    result.setProperty("totalIndexed", total.get());

    rs.add(result);
    return rs;
  }

  /**
   * Returns the list of property names to be indexed.
   *
   * @return Array of property names
   */
  private String[] calculateProperties(final CommandContext context) {
    if (propertyList == null) {
      return null;
    }
    return propertyList.stream().map(x -> x.getCompleteKey()).collect(Collectors.toList()).toArray(new String[] {});
  }

  @Override
  public void toString(final Map<String, Object> params, final StringBuilder builder) {
    builder.append("CREATE INDEX ");

    if (name != null)
      name.toString(params, builder);

    if (typeName != null) {
      builder.append(" ON ");
      typeName.toString(params, builder);
      builder.append(" (");
      boolean first = true;
      for (final Property prop : propertyList) {
        if (!first) {
          builder.append(", ");
        }
        if (prop.name != null) {
          prop.name.toString(params, builder);
        } else {
          prop.recordAttribute.toString(params, builder);
        }
        if (prop.byKey) {
          builder.append(" BY KEY");
        } else if (prop.byValue) {
          builder.append(" BY VALUE");
        }
        if (prop.collate != null) {
          builder.append(" COLLATE ");
          prop.collate.toString(params, builder);
        }
        first = false;
      }
      builder.append(")");
    }
    builder.append(" ");
    type.toString(params, builder);

    if (engine != null) {
      builder.append(" ENGINE ");
      engine.toString(params, builder);
    }
    if (metadata != null) {
      builder.append(" METADATA ");
      metadata.toString(params, builder);
    }

    if (nullStrategy != null) {
      builder.append(" NULL_STRATEGY ");
      builder.append(nullStrategy);
    }
    if (keyTypes != null && keyTypes.size() > 0) {
      boolean first = true;
      builder.append(" ");
      for (final Identifier keyType : keyTypes) {
        if (!first) {
          builder.append(",");
        }
        keyType.toString(params, builder);
        first = false;
      }
    }
  }

  @Override
  public CreateIndexStatement copy() {
    final CreateIndexStatement result = new CreateIndexStatement(-1);
    result.name = name == null ? null : name.copy();
    result.typeName = typeName == null ? null : typeName.copy();
    result.propertyList = propertyList == null ? null : propertyList.stream().map(x -> x.copy()).collect(Collectors.toList());
    result.type = type == null ? null : type.copy();
    result.nullStrategy = nullStrategy == null ? null : nullStrategy;
    result.keyTypes = keyTypes == null ? null : keyTypes.stream().map(x -> x.copy()).collect(Collectors.toList());
    result.engine = engine == null ? null : engine.copy();
    result.metadata = metadata == null ? null : metadata.copy();
    return result;
  }

  @Override
  protected Object[] getIdentityElements() {
    return new Object[] { name, typeName, propertyList, type, nullStrategy, keyTypes, engine, metadata };
  }

  public static class Property {
    protected Identifier      name;
    protected RecordAttribute recordAttribute;
    protected boolean         byKey   = false;
    protected boolean         byValue = false;
    protected Identifier      collate;

    public Property copy() {
      final Property result = new Property();
      result.name = name == null ? null : name.copy();
      result.recordAttribute = recordAttribute == null ? null : recordAttribute.copy();
      result.byKey = byKey;
      result.byValue = byValue;
      result.collate = collate == null ? null : collate.copy();
      return result;
    }

    @Override
    public boolean equals(final Object o) {
      if (this == o)
        return true;
      if (o == null || getClass() != o.getClass())
        return false;

      final Property property = (Property) o;

      if (byKey != property.byKey)
        return false;
      if (byValue != property.byValue)
        return false;
      if (!Objects.equals(name, property.name))
        return false;
      if (!Objects.equals(recordAttribute, property.recordAttribute))
        return false;
      return Objects.equals(collate, property.collate);
    }

    @Override
    public int hashCode() {
      int result = name != null ? name.hashCode() : 0;
      result = 31 * result + (recordAttribute != null ? recordAttribute.hashCode() : 0);
      result = 31 * result + (byKey ? 1 : 0);
      result = 31 * result + (byValue ? 1 : 0);
      result = 31 * result + (collate != null ? collate.hashCode() : 0);
      return result;
    }

    /**
     * returns the complete key to index, eg. property name or "property by key/value"
     *
     * @return
     */
    public String getCompleteKey() {
      final StringBuilder result = new StringBuilder();
      if (name != null)
        result.append(name.getStringValue());
      else if (recordAttribute != null)
        result.append(recordAttribute.getName());

      if (byKey) {
        result.append(" by key");
      }
      if (byValue) {
        result.append(" by value");
      }
      return result.toString();
    }

    @Override
    public String toString() {
      return name.toString();
    }
  }
}
/* JavaCC - OriginalChecksum=bd090e02c4346ad390a6b8c77f1b9dba (do not edit this line) */
