/*
 * 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.geode.cache.query.internal.parse;

import java.sql.Time;
import java.sql.Timestamp;
import java.util.StringTokenizer;

import antlr.Token;

import org.apache.geode.cache.query.QueryInvalidException;
import org.apache.geode.cache.query.QueryService;
import org.apache.geode.cache.query.internal.QCompiler;

public class ASTLiteral extends GemFireAST {
  private static final long serialVersionUID = 8374021603235812835L;

  public ASTLiteral() {}

  public ASTLiteral(Token t) {
    super(t);
  }


  private Object computeValue() throws QueryInvalidException {
    switch (getType()) {
      case OQLLexerTokenTypes.StringLiteral:
        return getString(getText(), '\'');
      case OQLLexerTokenTypes.NUM_INT:
        return getInt(getText());
      case OQLLexerTokenTypes.NUM_DOUBLE:
        return getDouble(getText());
      case OQLLexerTokenTypes.NUM_FLOAT:
        return getFloat(getText());
      case OQLLexerTokenTypes.NUM_LONG:
        return getLong(getText());
      case OQLLexerTokenTypes.LITERAL_nil:
      case OQLLexerTokenTypes.LITERAL_null:
        return null;
      case OQLLexerTokenTypes.LITERAL_undefined:
        return QueryService.UNDEFINED;
      case OQLLexerTokenTypes.LITERAL_true:
        return Boolean.TRUE;
      case OQLLexerTokenTypes.LITERAL_false:
        return Boolean.FALSE;
      case OQLLexerTokenTypes.LITERAL_char:
        return getChar(getFirstChild().getText());
      case OQLLexerTokenTypes.LITERAL_date:
        return getDate(getFirstChild().getText());
      case OQLLexerTokenTypes.LITERAL_timestamp:
        return getTimestamp(getFirstChild().getText());
      case OQLLexerTokenTypes.LITERAL_time:
        return getTime(getFirstChild().getText());
      default:
        throw new Error("unknown literal type: " + getType());
    }
  }

  public Integer getInt(String s) throws QueryInvalidException {
    try {
      return Integer.valueOf(s);
    } catch (NumberFormatException e) {
      throw new QueryInvalidException(
          String.format("unable to parse integer: %s", s), e);
    }
  }

  public Long getLong(String s) throws QueryInvalidException {
    char last = s.charAt(s.length() - 1);
    boolean strip = last == 'L' || last == 'l';

    if (strip) {
      s = s.substring(0, s.length() - 1);
    }
    try {
      return Long.valueOf(s);
    } catch (NumberFormatException e) {
      throw new QueryInvalidException(
          String.format("Unable to parse float %s", s));
    }
  }

  public Float getFloat(String s) throws QueryInvalidException {
    char last = s.charAt(s.length() - 1);
    boolean strip = last == 'F' || last == 'f';

    if (strip) {
      s = s.substring(0, s.length() - 1);
    }
    try {
      return new Float(s);
    } catch (NumberFormatException e) {
      throw new QueryInvalidException(s);
    }
  }


  public Double getDouble(String s) throws QueryInvalidException {
    char last = s.charAt(s.length() - 1);
    boolean strip = last == 'D' || last == 'd';
    if (strip) {
      s = s.substring(0, s.length() - 1);
    }
    try {
      return Double.valueOf(s);
    } catch (NumberFormatException e) {
      throw new QueryInvalidException(
          String.format("Unable to parse double %s", s));
    }
  }


  private String getString(String s, char delim) {
    // replace embedded double delims with delim
    StringBuilder buf = new StringBuilder(s.length());
    for (int i = 0; i < s.length(); i++) {
      char c = s.charAt(i);
      if (c == delim && i < (s.length() - 1) && s.charAt(i + 1) == delim) {
        i++;
      }
      buf.append(c);
    }
    return buf.toString();
  }

  @Override
  public void compile(QCompiler compiler) throws QueryInvalidException {
    Object value = computeValue();
    compiler.pushLiteral(value);
  }



  private Character getChar(String s) {
    if (s.length() != 1) {
      throw new QueryInvalidException(
          "Illegal format for CHAR literal. A character literal must have exactly one character");
    }
    return s.charAt(0);
  }

  private java.sql.Date getDate(String s) {
    try {
      java.sql.Date date = java.sql.Date.valueOf(s);
      // passed valueOf, now verify args are really in range
      int firstDash = s.indexOf('-');
      int secondDash = s.indexOf('-', firstDash + 1);
      int month = Integer.parseInt(s.substring(firstDash + 1, secondDash));
      int day = Integer.parseInt(s.substring(secondDash + 1));
      if (month < 1 || month > 12) {
        throw new QueryInvalidException(
            "Month must be 1..12 in DATE literal");
      }
      if (day < 1 || day > 31) {
        throw new QueryInvalidException(
            "Day must be 1..31 in DATE literal");
      }
      return date;
    } catch (IllegalArgumentException e) {
      throw new QueryInvalidException(
          String.format("Illegal format for DATE literal: %s . Expected format is yyyy-mm-dd",
              s));
    }
  }


  private java.sql.Time getTime(String s) {
    try {
      Time time = java.sql.Time.valueOf(s);
      // passed valueOf, now verify args are really in range
      int firstColon = s.indexOf(':');
      int secondColon = s.indexOf(':', firstColon + 1);
      int hour = Integer.parseInt(s.substring(0, firstColon));
      int minute = Integer.parseInt(s.substring(firstColon + 1, secondColon));
      int second = Integer.parseInt(s.substring(secondColon + 1));
      if (hour < 0 || hour > 23) {
        throw new QueryInvalidException(
            "Hour must be 0..23 in TIME literal");
      }
      if (minute < 0 || minute > 59) {
        throw new QueryInvalidException(
            "Minute must be 0..59 in TIME literal");
      }
      if (second < 0 || second > 59) {
        throw new QueryInvalidException(
            "Second must be 0..59 in TIME literal");
      }
      return time;
    } catch (IllegalArgumentException e) {
      throw new QueryInvalidException(
          String.format("Illegal format for TIME literal: %s . Expected format is hh:mm:ss",
              s));
    }
  }

  private java.sql.Timestamp getTimestamp(String s) {
    try {
      Timestamp timestamp = java.sql.Timestamp.valueOf(s);
      // passed valueOf, now make sure parameters are in range
      // first rule out negative numbers by counting the dashes
      int count = 0;
      int index = -1;
      while ((index = s.indexOf('-', index + 1)) >= 0) {
        count++;
      }

      if (count > 2) {
        throw new QueryInvalidException(
            "Negative numbers not allowed in TIMESTAMP literal");
      }

      // now just get the numbers, ignoring delimiters
      StringTokenizer tokenizer = new StringTokenizer(s, ":-\t\n\r\f ");
      // skip year, anything is valid
      tokenizer.nextToken();
      int month = Integer.parseInt(tokenizer.nextToken());
      if (month < 1 || month > 12) {
        throw new QueryInvalidException(
            "Month must be 1..12 in TIMESTAMP literal");
      }
      int day = Integer.parseInt(tokenizer.nextToken());
      if (day < 1 || day > 31) {
        throw new QueryInvalidException(
            "Day must be 1..31 in TIMESTAMP literal");
      }
      int hour = Integer.parseInt(tokenizer.nextToken());
      if (hour < 0 || hour > 23) {
        throw new QueryInvalidException(
            "Hour must be 0..23 in TIMESTAMP literal");
      }
      int minute = Integer.parseInt(tokenizer.nextToken());
      if (minute < 0 || minute > 59) {
        throw new QueryInvalidException(
            "Minute must be 0..59 in TIMESTAMP literal");
      }
      String sec_s = tokenizer.nextToken();
      int period = sec_s.indexOf('.');
      int second;
      if (period >= 0) {
        second = Integer.parseInt(sec_s.substring(0, period));
      } else {
        second = Integer.parseInt(sec_s);
      }
      if (second < 0 || second > 59) {
        throw new QueryInvalidException(
            "Second must be 0..59 in TIMESTAMP literal");
      }
      return timestamp;
    } catch (IllegalArgumentException e) {
      throw new QueryInvalidException(
          String.format(
              "Illegal format for TIMESTAMP literal: %s . Expected format is yyyy-mm-dd hh:mm:ss.fffffffff",
              s));
    }
  }
}
