/*
 * 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.CodedInputStream;
import com.google.protobuf.Descriptors.FieldDescriptor;
import org.jruby.*;
import org.jruby.anno.JRubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Block;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;

@JRubyClass(name = "FieldDescriptor")
public class RubyFieldDescriptor extends RubyObject {
  public static void createRubyFieldDescriptor(Ruby runtime) {
    RubyModule mProtobuf = runtime.getClassFromPath("Google::Protobuf");
    RubyClass cFieldDescriptor =
        mProtobuf.defineClassUnder(
            "FieldDescriptor",
            runtime.getObject(),
            new ObjectAllocator() {
              @Override
              public IRubyObject allocate(Ruby runtime, RubyClass klazz) {
                return new RubyFieldDescriptor(runtime, klazz);
              }
            });
    cFieldDescriptor.defineAnnotatedMethods(RubyFieldDescriptor.class);
  }

  public RubyFieldDescriptor(Ruby runtime, RubyClass klazz) {
    super(runtime, klazz);
  }

  /*
   * call-seq:
   *     FieldDescriptor.default => default
   *
   * Returns this field's default, as a Ruby object, or nil if not yet set.
   */
  // VALUE FieldDescriptor_default(VALUE _self) {
  //   DEFINE_SELF(FieldDescriptor, self, _self);
  //   return layout_get_default(self->fielddef);
  // }

  /*
   * call-seq:
   *     FieldDescriptor.label => label
   *
   * Returns this field's label (i.e., plurality), as a Ruby symbol.
   *
   * Valid field labels are:
   *     :optional, :repeated
   */
  @JRubyMethod(name = "label")
  public IRubyObject getLabel(ThreadContext context) {
    if (label == null) {
      calculateLabel(context);
    }
    return label;
  }

  /*
   * call-seq:
   *     FieldDescriptor.has_presence? => bool
   *
   * Returns whether this field tracks presence.
   */
  @JRubyMethod(name = "has_presence?")
  public IRubyObject hasPresence(ThreadContext context) {
    return this.descriptor.hasPresence() ? context.runtime.getTrue() : context.runtime.getFalse();
  }

  /*
   * call-seq:
   *     FieldDescriptor.is_packed? => bool
   *
   * Returns whether this is a repeated field that uses packed encoding.
   */
  @JRubyMethod(name = "is_packed?")
  public IRubyObject isPacked(ThreadContext context) {
    return this.descriptor.isPacked() ? context.runtime.getTrue() : context.runtime.getFalse();
  }

  /*
   * call-seq:
   *     FieldDescriptor.name => name
   *
   * Returns the name of this field as a Ruby String, or nil if it is not set.
   */
  @JRubyMethod(name = "name")
  public IRubyObject getName(ThreadContext context) {
    return this.name;
  }

  protected void setName(IRubyObject name) {
    this.name = name;
  }

  /*
   * call-seq:
   *     FieldDescriptor.subtype => message_or_enum_descriptor
   *
   * Returns the message or enum descriptor corresponding to this field's type if
   * it is a message or enum field, respectively, or nil otherwise. Cannot be
   * called *until* the containing message type is added to a pool (and thus
   * resolved).
   */
  @JRubyMethod(name = "subtype")
  public IRubyObject getSubtype(ThreadContext context) {
    if (subtype == null) {
      calculateSubtype(context);
    }
    return subtype;
  }

  /*
   * call-seq:
   *     FieldDescriptor.type => type
   *
   * Returns this field's type, as a Ruby symbol, or nil if not yet set.
   *
   * Valid field types are:
   *     :int32, :int64, :uint32, :uint64, :float, :double, :bool, :string,
   *     :bytes, :message.
   */
  @JRubyMethod(name = "type")
  public IRubyObject getType(ThreadContext context) {
    return Utils.fieldTypeToRuby(context, descriptor.getType());
  }

  /*
   * call-seq:
   *     FieldDescriptor.number => number
   *
   * Returns the tag number for this field.
   */
  @JRubyMethod(name = "number")
  public IRubyObject getNumber(ThreadContext context) {
    return this.number;
  }

  /*
   * call-seq:
   *     FieldDescriptor.submsg_name => submsg_name
   *
   * Returns the name of the message or enum type corresponding to this field, if
   * it is a message or enum field (respectively), or nil otherwise. This type
   * name will be resolved within the context of the pool to which the containing
   * message type is added.
   */
  // VALUE FieldDescriptor_submsg_name(VALUE _self) {
  //   DEFINE_SELF(FieldDescriptor, self, _self);
  //   switch (upb_fielddef_type(self->fielddef)) {
  //     case UPB_TYPE_ENUM:
  //       return rb_str_new2(
  //           upb_enumdef_fullname(upb_fielddef_enumsubdef(self->fielddef)));
  //     case UPB_TYPE_MESSAGE:
  //       return rb_str_new2(
  //           upb_msgdef_fullname(upb_fielddef_msgsubdef(self->fielddef)));
  //     default:
  //       return Qnil;
  //   }
  // }
  /*
   * call-seq:
   *     FieldDescriptor.submsg_name = submsg_name
   *
   * Sets the name of the message or enum type corresponding to this field, if it
   * is a message or enum field (respectively). This type name will be resolved
   * within the context of the pool to which the containing message type is added.
   * Cannot be called on field that are not of message or enum type, or on fields
   * that are part of a message type already added to a pool.
   */
  // @JRubyMethod(name = "submsg_name=")
  // public IRubyObject setSubmsgName(ThreadContext context, IRubyObject name) {
  //     this.builder.setTypeName("." + Utils.escapeIdentifier(name.asJavaString()));
  //     return context.runtime.getNil();
  // }

  /*
   * call-seq:
   *     FieldDescriptor.clear(message)
   *
   * Clears the field from the message if it's set.
   */
  @JRubyMethod(name = "clear")
  public IRubyObject clearValue(ThreadContext context, IRubyObject message) {
    return ((RubyMessage) message).clearField(context, descriptor);
  }

  /*
   * call-seq:
   *     FieldDescriptor.get(message) => value
   *
   * Returns the value set for this field on the given message. Raises an
   * exception if message is of the wrong type.
   */
  @JRubyMethod(name = "get")
  public IRubyObject getValue(ThreadContext context, IRubyObject message) {
    return ((RubyMessage) message).getField(context, descriptor);
  }

  /*
   * call-seq:
   *     FieldDescriptor.has?(message) => boolean
   *
   * Returns whether the value is set on the given message. Raises an
   * exception when calling for fields that do not have presence.
   */
  @JRubyMethod(name = "has?")
  public IRubyObject has(ThreadContext context, IRubyObject message) {
    return ((RubyMessage) message).hasField(context, descriptor);
  }

  /*
   * call-seq:
   *     FieldDescriptor.set(message, value)
   *
   * Sets the value corresponding to this field to the given value on the given
   * message. Raises an exception if message is of the wrong type. Performs the
   * ordinary type-checks for field setting.
   */
  @JRubyMethod(name = "set")
  public IRubyObject setValue(ThreadContext context, IRubyObject message, IRubyObject value) {
    ((RubyMessage) message).setField(context, this, value);
    return context.nil;
  }

  @JRubyMethod
  public IRubyObject options(ThreadContext context) {
    RubyDescriptor fieldOptionsDescriptor =
        (RubyDescriptor)
            pool.lookup(context, context.runtime.newString("google.protobuf.FieldOptions"));
    RubyClass fieldOptionsClass = (RubyClass) fieldOptionsDescriptor.msgclass(context);
    RubyMessage msg = (RubyMessage) fieldOptionsClass.newInstance(context, Block.NULL_BLOCK);
    return msg.decodeBytes(
        context,
        msg,
        CodedInputStream.newInstance(
            descriptor.getOptions().toByteString().toByteArray()), /*freeze*/
        true);
  }

  /*
   * call-seq:
   *     FieldDescriptor.to_proto => FieldDescriptorProto
   *
   * Returns the `FieldDescriptorProto` of this `FieldDescriptor`.
   */
  @JRubyMethod(name = "to_proto")
  public IRubyObject toProto(ThreadContext context) {
    RubyDescriptorPool pool = (RubyDescriptorPool) RubyDescriptorPool.generatedPool(null, null);
    RubyDescriptor fieldDescriptorProto =
        (RubyDescriptor)
            pool.lookup(context, context.runtime.newString("google.protobuf.FieldDescriptorProto"));
    RubyClass msgClass = (RubyClass) fieldDescriptorProto.msgclass(context);
    RubyMessage msg = (RubyMessage) msgClass.newInstance(context, Block.NULL_BLOCK);
    return msg.decodeBytes(
        context,
        msg,
        CodedInputStream.newInstance(descriptor.toProto().toByteString().toByteArray()), /*freeze*/
        true);
  }

  protected void setDescriptor(
      ThreadContext context, FieldDescriptor descriptor, RubyDescriptorPool pool) {
    this.descriptor = descriptor;
    this.name = context.runtime.newString(descriptor.getName());
    this.pool = pool;
  }

  protected FieldDescriptor getDescriptor() {
    return descriptor;
  }

  private void calculateLabel(ThreadContext context) {
    if (descriptor.isRepeated()) {
      this.label = context.runtime.newSymbol("repeated");
    } else if (descriptor.isRequired()) {
      this.label = context.runtime.newSymbol("required");
    } else if (descriptor.isOptional()) {
      this.label = context.runtime.newSymbol("optional");
    } else {
      this.label = context.nil;
    }
  }

  private void calculateSubtype(ThreadContext context) {
    FieldDescriptor.Type fdType = descriptor.getType();
    if (fdType == FieldDescriptor.Type.MESSAGE) {
      RubyString messageName = context.runtime.newString(descriptor.getMessageType().getFullName());
      this.subtype = pool.lookup(context, messageName);
    } else if (fdType == FieldDescriptor.Type.ENUM) {
      RubyString enumName = context.runtime.newString(descriptor.getEnumType().getFullName());
      this.subtype = pool.lookup(context, enumName);
    } else {
      this.subtype = context.nil;
    }
  }

  private static final String DOT = ".";

  private FieldDescriptor descriptor;
  private IRubyObject name;
  private IRubyObject label;
  private IRubyObject number;
  private IRubyObject subtype;
  private RubyDescriptorPool pool;
}
