/*
 * Protocol Buffers - Google's data interchange format
 * Copyright 2014 Google Inc.  All rights reserved.
 * https://developers.google.com/protocol-buffers/
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package com.google.protobuf.jruby;

import com.google.protobuf.ByteString;
import com.google.protobuf.DescriptorProtos.FieldDescriptorProto;
import com.google.protobuf.Descriptors.FieldDescriptor;
import java.math.BigInteger;
import org.jcodings.Encoding;
import org.jcodings.specific.ASCIIEncoding;
import org.jcodings.specific.UTF8Encoding;
import org.jruby.*;
import org.jruby.exceptions.RaiseException;
import org.jruby.ext.bigdecimal.RubyBigDecimal;
import org.jruby.runtime.Block;
import org.jruby.runtime.Helpers;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;

public class Utils {
  public static FieldDescriptor.Type rubyToFieldType(IRubyObject typeClass) {
    return FieldDescriptor.Type.valueOf(typeClass.asJavaString().toUpperCase());
  }

  public static IRubyObject fieldTypeToRuby(ThreadContext context, FieldDescriptor.Type type) {
    return fieldTypeToRuby(context, type.name());
  }

  public static IRubyObject fieldTypeToRuby(ThreadContext context, FieldDescriptorProto.Type type) {
    return fieldTypeToRuby(context, type.name());
  }

  private static IRubyObject fieldTypeToRuby(ThreadContext context, String typeName) {

    return context.runtime.newSymbol(typeName.replace("TYPE_", "").toLowerCase());
  }

  public static IRubyObject checkType(
      ThreadContext context,
      FieldDescriptor.Type fieldType,
      String fieldName,
      IRubyObject value,
      RubyModule typeClass) {
    Ruby runtime = context.runtime;

    switch (fieldType) {
      case SFIXED32:
      case SFIXED64:
      case FIXED64:
      case SINT64:
      case SINT32:
      case FIXED32:
      case INT32:
      case INT64:
      case UINT32:
      case UINT64:
        if (!isRubyNum(value))
          throw createExpectedTypeError(context, "number", "integral", fieldName, value);

        if (value instanceof RubyFloat) {
          double doubleVal = RubyNumeric.num2dbl(value);
          if (Math.floor(doubleVal) != doubleVal) {
            throw runtime.newRangeError(
                "Non-integral floating point value assigned to integer field '"
                    + fieldName
                    + "' (given "
                    + value.getMetaClass()
                    + ").");
          }
        }
        if (fieldType == FieldDescriptor.Type.UINT32
            || fieldType == FieldDescriptor.Type.UINT64
            || fieldType == FieldDescriptor.Type.FIXED32
            || fieldType == FieldDescriptor.Type.FIXED64) {
          if (((RubyNumeric) value).isNegative()) {
            throw runtime.newRangeError(
                "Assigning negative value to unsigned integer field '"
                    + fieldName
                    + "' (given "
                    + value.getMetaClass()
                    + ").");
          }
        }

        switch (fieldType) {
          case INT32:
            RubyNumeric.num2int(value);
            break;
          case UINT32:
          case FIXED32:
            num2uint(value);
            break;
          case UINT64:
          case FIXED64:
            num2ulong(context.runtime, value);
            break;
          default:
            RubyNumeric.num2long(value);
            break;
        }
        break;
      case FLOAT:
        if (!isRubyNum(value))
          throw createExpectedTypeError(context, "number", "float", fieldName, value);
        break;
      case DOUBLE:
        if (!isRubyNum(value))
          throw createExpectedTypeError(context, "number", "double", fieldName, value);
        break;
      case BOOL:
        if (!(value instanceof RubyBoolean))
          throw createInvalidTypeError(context, "boolean", fieldName, value);
        break;
      case BYTES:
        value = validateAndEncodeString(context, "bytes", fieldName, value, ASCIIEncoding.INSTANCE);
        break;
      case STRING:
        value =
            validateAndEncodeString(
                context, "string", fieldName, symToString(value), UTF8Encoding.INSTANCE);
        break;
      case MESSAGE:
        if (value.getMetaClass() != typeClass) {
          // See if we can convert the value before flagging it as invalid
          String className = typeClass.getName();

          if (className.equals("Google::Protobuf::Timestamp") && value instanceof RubyTime) {
            RubyTime rt = (RubyTime) value;
            RubyHash timestampArgs =
                Helpers.constructHash(
                    runtime,
                    runtime.newString("nanos"),
                    rt.nsec(),
                    false,
                    runtime.newString("seconds"),
                    rt.to_i(),
                    false);
            return ((RubyClass) typeClass).newInstance(context, timestampArgs, Block.NULL_BLOCK);

          } else if (className.equals("Google::Protobuf::Duration")
              && value instanceof RubyNumeric) {
            IRubyObject seconds;
            if (value instanceof RubyFloat) {
              seconds = ((RubyFloat) value).truncate(context);
            } else if (value instanceof RubyRational) {
              seconds = ((RubyRational) value).to_i(context);
            } else if (value instanceof RubyBigDecimal) {
              seconds = ((RubyBigDecimal) value).to_int(context);
            } else {
              seconds = ((RubyInteger) value).to_i();
            }

            IRubyObject nanos = ((RubyNumeric) value).remainder(context, RubyFixnum.one(runtime));
            if (nanos instanceof RubyFloat) {
              nanos = ((RubyFloat) nanos).op_mul(context, 1000000000);
            } else if (nanos instanceof RubyRational) {
              nanos = ((RubyRational) nanos).op_mul(context, runtime.newFixnum(1000000000));
            } else if (nanos instanceof RubyBigDecimal) {
              nanos = ((RubyBigDecimal) nanos).op_mul(context, runtime.newFixnum(1000000000));
            } else {
              nanos = ((RubyInteger) nanos).op_mul(context, 1000000000);
            }

            RubyHash durationArgs =
                Helpers.constructHash(
                    runtime,
                    runtime.newString("nanos"),
                    ((RubyNumeric) nanos).round(context),
                    false,
                    runtime.newString("seconds"),
                    seconds,
                    false);
            return ((RubyClass) typeClass).newInstance(context, durationArgs, Block.NULL_BLOCK);
          }

          // Not able to convert so flag as invalid
          throw createTypeError(
              context,
              "Invalid type "
                  + value.getMetaClass()
                  + " to assign to submessage field '"
                  + fieldName
                  + "'.");
        }

        break;
      case ENUM:
        boolean isValid =
            ((RubyEnumDescriptor) typeClass.getInstanceVariable(DESCRIPTOR_INSTANCE_VAR))
                .isValidValue(context, value);
        if (!isValid) {
          throw runtime.newRangeError("Unknown symbol value for enum field '" + fieldName + "'.");
        }
        break;
      default:
        break;
    }
    return value;
  }

  public static IRubyObject wrapPrimaryValue(
      ThreadContext context, FieldDescriptor.Type fieldType, Object value) {
    return wrapPrimaryValue(context, fieldType, value, false);
  }

  public static IRubyObject wrapPrimaryValue(
      ThreadContext context, FieldDescriptor.Type fieldType, Object value, boolean encodeBytes) {
    Ruby runtime = context.runtime;
    switch (fieldType) {
      case INT32:
      case SFIXED32:
      case SINT32:
        return runtime.newFixnum((Integer) value);
      case SFIXED64:
      case SINT64:
      case INT64:
        return runtime.newFixnum((Long) value);
      case FIXED32:
      case UINT32:
        return runtime.newFixnum(((Integer) value) & (-1l >>> 32));
      case FIXED64:
      case UINT64:
        long ret = (Long) value;
        return ret >= 0
            ? runtime.newFixnum(ret)
            : RubyBignum.newBignum(runtime, UINT64_COMPLEMENTARY.add(new BigInteger(ret + "")));
      case FLOAT:
        return runtime.newFloat((Float) value);
      case DOUBLE:
        return runtime.newFloat((Double) value);
      case BOOL:
        return (Boolean) value ? runtime.getTrue() : runtime.getFalse();
      case BYTES:
        {
          IRubyObject wrapped =
              encodeBytes
                  ? RubyString.newString(
                      runtime,
                      new ByteList(((ByteString) value).toByteArray()),
                      ASCIIEncoding.INSTANCE)
                  : RubyString.newString(runtime, ((ByteString) value).toByteArray());
          wrapped.setFrozen(true);
          return wrapped;
        }
      case STRING:
        {
          IRubyObject wrapped = runtime.newString(value.toString());
          wrapped.setFrozen(true);
          return wrapped;
        }
      default:
        return runtime.getNil();
    }
  }

  public static int num2uint(IRubyObject value) {
    long longVal = RubyNumeric.num2long(value);
    if (longVal > UINT_MAX)
      throw value
          .getRuntime()
          .newRangeError("Integer " + longVal + " too big to convert to 'unsigned int'");
    long num = longVal;
    if (num > Integer.MAX_VALUE || num < Integer.MIN_VALUE)
      // encode to UINT32
      num = (-longVal ^ (-1l >>> 32)) + 1;
    RubyNumeric.checkInt(value, num);
    return (int) num;
  }

  public static long num2ulong(Ruby runtime, IRubyObject value) {
    if (value instanceof RubyFloat) {
      RubyBignum bignum = RubyBignum.newBignum(runtime, ((RubyFloat) value).getDoubleValue());
      return RubyBignum.big2ulong(bignum);
    } else if (value instanceof RubyBignum) {
      return RubyBignum.big2ulong((RubyBignum) value);
    } else {
      return RubyNumeric.num2long(value);
    }
  }

  /*
   * Helper to make it easier to support symbols being passed instead of strings
   */
  public static IRubyObject symToString(IRubyObject sym) {
    if (sym instanceof RubySymbol) {
      return ((RubySymbol) sym).id2name();
    }
    return sym;
  }

  public static void checkNameAvailability(ThreadContext context, String name) {
    if (context.runtime.getObject().getConstantAt(name) != null)
      throw context.runtime.newNameError(name + " is already defined", name);
  }

  public static boolean isMapEntry(FieldDescriptor fieldDescriptor) {
    return fieldDescriptor.getType() == FieldDescriptor.Type.MESSAGE
        && fieldDescriptor.isRepeated()
        && fieldDescriptor.getMessageType().getOptions().getMapEntry();
  }

  public static RaiseException createTypeError(ThreadContext context, String message) {
    if (cTypeError == null) {
      cTypeError = (RubyClass) context.runtime.getClassFromPath("Google::Protobuf::TypeError");
    }
    return RaiseException.from(context.runtime, cTypeError, message);
  }

  public static RaiseException createExpectedTypeError(
      ThreadContext context, String type, String fieldType, String fieldName, IRubyObject value) {
    return createTypeError(
        context,
        String.format(
            EXPECTED_TYPE_ERROR_FORMAT, type, fieldType, fieldName, value.getMetaClass()));
  }

  public static RaiseException createInvalidTypeError(
      ThreadContext context, String fieldType, String fieldName, IRubyObject value) {
    return createTypeError(
        context,
        String.format(INVALID_TYPE_ERROR_FORMAT, fieldType, fieldName, value.getMetaClass()));
  }

  protected static boolean isRubyNum(Object value) {
    return value instanceof RubyFixnum || value instanceof RubyFloat || value instanceof RubyBignum;
  }

  protected static void validateTypeClass(
      ThreadContext context, FieldDescriptor.Type type, IRubyObject value) {
    Ruby runtime = context.runtime;
    if (!(value instanceof RubyModule)) {
      throw runtime.newArgumentError("TypeClass has incorrect type");
    }
    RubyModule klass = (RubyModule) value;
    IRubyObject descriptor = klass.getInstanceVariable(DESCRIPTOR_INSTANCE_VAR);
    if (descriptor.isNil()) {
      throw runtime.newArgumentError(
          "Type class has no descriptor. Please pass a "
              + "class or enum as returned by the DescriptorPool.");
    }
    if (type == FieldDescriptor.Type.MESSAGE) {
      if (!(descriptor instanceof RubyDescriptor)) {
        throw runtime.newArgumentError("Descriptor has an incorrect type");
      }
    } else if (type == FieldDescriptor.Type.ENUM) {
      if (!(descriptor instanceof RubyEnumDescriptor)) {
        throw runtime.newArgumentError("Descriptor has an incorrect type");
      }
    }
  }

  private static IRubyObject validateAndEncodeString(
      ThreadContext context,
      String fieldType,
      String fieldName,
      IRubyObject value,
      Encoding encoding) {
    if (!(value instanceof RubyString))
      throw createInvalidTypeError(context, fieldType, fieldName, value);

    value =
        ((RubyString) value)
            .encode(
                context,
                context.runtime.getEncodingService().convertEncodingToRubyEncoding(encoding));
    value.setFrozen(true);
    return value;
  }

  public static final String DESCRIPTOR_INSTANCE_VAR = "@descriptor";

  public static final String EQUAL_SIGN = "=";

  private static final BigInteger UINT64_COMPLEMENTARY =
      new BigInteger("18446744073709551616"); // Math.pow(2, 64)

  private static final String EXPECTED_TYPE_ERROR_FORMAT =
      "Expected %s type for %s field '%s' (given %s).";
  private static final String INVALID_TYPE_ERROR_FORMAT =
      "Invalid argument for %s field '%s' (given %s).";

  private static final long UINT_MAX = 0xffffffffl;

  private static RubyClass cTypeError;
}
