/*
 * Copyright 2016 Sai Pullabhotla.
 *
 * 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 com.jmethods.catatumbo.impl;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZonedDateTime;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import com.google.cloud.Timestamp;
import com.google.cloud.datastore.Blob;
import com.google.cloud.datastore.Cursor;
import com.google.cloud.datastore.GqlQuery;
import com.jmethods.catatumbo.DatastoreCursor;
import com.jmethods.catatumbo.DatastoreKey;
import com.jmethods.catatumbo.GeoLocation;
import com.jmethods.catatumbo.mappers.LocalDateTimeMapper;
import com.jmethods.catatumbo.mappers.LocalTimeMapper;

/**
 * Utility methods for GQL Queries.
 * 
 * @author Sai Pullabhotla
 *
 */
public class QueryUtils {

  /**
   * Hide the default constructor.
   */
  private QueryUtils() {
    // Do nothing
  }

  /**
   * Applies the given positional bindings to the given query builder.
   * 
   * @param queryBuilder
   *          the query builder
   * @param positionalBindings
   *          the positional bindings.
   */
  static void applyPositionalBindings(GqlQuery.Builder<?> queryBuilder,
      Object... positionalBindings) {
    if (positionalBindings != null) {
      for (Object binding : positionalBindings) {
        addPositionalBinding(queryBuilder, binding);
      }
    }
  }

  /**
   * Applies the given positional bindings to the given query builder.
   * 
   * @param queryBuilder
   *          the query builder
   * @param positionalBindings
   *          the positional bindings.
   */
  static void applyPositionalBindings(GqlQuery.Builder<?> queryBuilder,
      List<Object> positionalBindings) {
    if (positionalBindings != null) {
      for (Object binding : positionalBindings) {
        addPositionalBinding(queryBuilder, binding);
      }
    }
  }

  /**
   * Adds the given binding to the given query builder's to the list of positional bindings.
   * 
   * @param queryBuilder
   *          the query builder
   * @param binding
   *          the positional binding to add
   */
  static void addPositionalBinding(GqlQuery.Builder<?> queryBuilder, Object binding) {
    if (binding == null) {
      throw new IllegalArgumentException("binding cannot be null. Use IS NULL in your query");
    }
    if (binding instanceof Short) {
      queryBuilder.addBinding((short) binding);
    } else if (binding instanceof Integer) {
      queryBuilder.addBinding((int) binding);
    } else if (binding instanceof Long) {
      queryBuilder.addBinding((long) binding);
    } else if (binding instanceof Float) {
      queryBuilder.addBinding((float) binding);
    } else if (binding instanceof Double) {
      queryBuilder.addBinding((double) binding);
    } else if (binding instanceof Boolean) {
      queryBuilder.addBinding((boolean) binding);
    } else if (binding instanceof Character) {
      queryBuilder.addBinding(String.valueOf((char) binding));
    } else if (binding instanceof String) {
      queryBuilder.addBinding((String) binding);
    } else if (binding instanceof Calendar) {
      queryBuilder.addBinding(toTimestamp((Calendar) binding));
    } else if (binding instanceof Date) {
      queryBuilder.addBinding(toTimestamp((Date) binding));
    } else if (binding instanceof LocalDate) {
      queryBuilder.addBinding(((LocalDate) binding).toString());
    } else if (binding instanceof LocalTime) {
      queryBuilder.addBinding(((LocalTime) binding).format(LocalTimeMapper.FORMATTER));
    } else if (binding instanceof LocalDateTime) {
      queryBuilder.addBinding(((LocalDateTime) binding).format(LocalDateTimeMapper.FORMATTER));
    } else if (binding instanceof OffsetDateTime) {
      queryBuilder.addBinding(toTimestamp((OffsetDateTime) binding));
    } else if (binding instanceof ZonedDateTime) {
      queryBuilder.addBinding(toTimestamp((ZonedDateTime) binding));
    } else if (binding instanceof byte[]) {
      queryBuilder.addBinding(Blob.copyFrom((byte[]) binding));
    } else if (binding instanceof DatastoreKey) {
      queryBuilder.addBinding(((DatastoreKey) binding).nativeKey());
    } else if (binding instanceof DatastoreCursor) {
      queryBuilder.addBinding(Cursor.fromUrlSafe(((DatastoreCursor) binding).getEncoded()));
    } else if (binding instanceof GeoLocation) {
      // TODO no support for GeoLocation in the gcloud API
    }
  }

  /**
   * Applies the given positional bindings to the given query builder.
   * 
   * @param queryBuilder
   *          the query builder
   * @param namedBindings
   *          the named bindings to apply
   */
  static void applyNamedBindings(GqlQuery.Builder<?> queryBuilder,
      Map<String, Object> namedBindings) {
    if (namedBindings != null) {
      for (Map.Entry<String, Object> entry : namedBindings.entrySet()) {
        String bindingName = entry.getKey();
        Object bindingValue = entry.getValue();
        if (bindingValue instanceof Short) {
          queryBuilder.setBinding(bindingName, (short) bindingValue);
        } else if (bindingValue instanceof Integer) {
          queryBuilder.setBinding(bindingName, (int) bindingValue);
        } else if (bindingValue instanceof Long) {
          queryBuilder.setBinding(bindingName, (long) bindingValue);
        } else if (bindingValue instanceof Float) {
          queryBuilder.setBinding(bindingName, (float) bindingValue);
        } else if (bindingValue instanceof Double) {
          queryBuilder.setBinding(bindingName, (double) bindingValue);
        } else if (bindingValue instanceof Boolean) {
          queryBuilder.setBinding(bindingName, (boolean) bindingValue);
        } else if (bindingValue instanceof String) {
          queryBuilder.setBinding(bindingName, (String) bindingValue);
        } else if (bindingValue instanceof Calendar) {
          queryBuilder.setBinding(bindingName, toTimestamp((Calendar) bindingValue));
        } else if (bindingValue instanceof Date) {
          queryBuilder.setBinding(bindingName, toTimestamp((Date) bindingValue));
        } else if (bindingValue instanceof LocalDate) {
          queryBuilder.setBinding(bindingName, ((LocalDate) bindingValue).toString());
        } else if (bindingValue instanceof LocalTime) {
          queryBuilder.setBinding(bindingName,
              ((LocalTime) bindingValue).format(LocalTimeMapper.FORMATTER));
        } else if (bindingValue instanceof LocalDateTime) {
          queryBuilder.setBinding(bindingName,
              ((LocalDateTime) bindingValue).format(LocalDateTimeMapper.FORMATTER));
        } else if (bindingValue instanceof OffsetDateTime) {
          queryBuilder.setBinding(bindingName, toTimestamp((OffsetDateTime) bindingValue));
        } else if (bindingValue instanceof ZonedDateTime) {
          queryBuilder.setBinding(bindingName, toTimestamp((ZonedDateTime) bindingValue));
        } else if (bindingValue instanceof byte[]) {
          queryBuilder.setBinding(bindingName, Blob.copyFrom((byte[]) bindingValue));
        } else if (bindingValue instanceof DatastoreKey) {
          queryBuilder.setBinding(bindingName, ((DatastoreKey) bindingValue).nativeKey());
        } else if (bindingValue instanceof DatastoreCursor) {
          queryBuilder.setBinding(bindingName,
              Cursor.fromUrlSafe(((DatastoreCursor) bindingValue).getEncoded()));
        } else if (bindingValue instanceof GeoLocation) {
          // TODO no support for GeoLocation in the gcloud API
        }
      }
    }
  }

  /**
   * Converts the given Calendar to a Timestamp.
   * 
   * @param calendar
   *          the Calendar to convert
   * @return Timestamp object that is equivalent to the given Calendar.
   */
  private static Timestamp toTimestamp(Calendar calendar) {
    return Timestamp.of(calendar.getTime());
  }

  /**
   * Converts the given Date to a Timestamp.
   * 
   * @param date
   *          the Date to convert
   * @return Timestamp object that is equivalent to the given Date.
   */
  private static Timestamp toTimestamp(Date date) {
    return Timestamp.of(date);
  }

  /**
   * Converts the given OffsetDateTime to a Timestamp.
   * 
   * @param offsetDateTime
   *          the OffsetDateTime to convert
   * @return Timestamp object that is equivalent to the given OffsetDateTime.
   */
  private static Timestamp toTimestamp(OffsetDateTime offsetDateTime) {
    long seconds = offsetDateTime.toEpochSecond();
    int nanos = offsetDateTime.getNano();
    long microseconds = TimeUnit.SECONDS.toMicros(seconds) + TimeUnit.NANOSECONDS.toMicros(nanos);
    return Timestamp.ofTimeMicroseconds(microseconds);
  }

  /**
   * Converts the given OffsetDateTime to a Timestamp.
   * 
   * @param zonedDateTime
   *          the {@link ZonedDateTime} to convert
   * @return Timestamp object that is equivalent to the given OffsetDateTime.
   */
  private static Timestamp toTimestamp(ZonedDateTime zonedDateTime) {
    long seconds = zonedDateTime.toEpochSecond();
    int nanos = zonedDateTime.getNano();
    long microseconds = TimeUnit.SECONDS.toMicros(seconds) + TimeUnit.NANOSECONDS.toMicros(nanos);
    return Timestamp.ofTimeMicroseconds(microseconds);
  }

}