/*
 * Copyright 2019 Google LLC
 *
 * 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 io.perfmark.impl;

import java.util.Arrays;
import javax.annotation.Nullable;

public final class Mark {
  // TODO: make sure these match the values in Impl
  public static final String NO_TAG_NAME = "";
  public static final long NO_TAG_ID = Long.MIN_VALUE;
  public static final long NO_LINK_ID = Long.MIN_VALUE;

  public static final long NO_NANOTIME = 0;

  private static final long N0 = 0;
  private static final String S0 = null;
  private static final Marker M0 = null;

  private final long generation;

  private final long n1;
  private final long n2;
  private final long n3;

  @Nullable private final String s1;
  @Nullable private final String s2;
  @Nullable private final String s3;

  @Nullable private final Marker m;

  private final Operation operation;

  public static Mark taskStart(long generation, long nanoTime, String name) {
    return new Mark(nanoTime, N0, N0, name, S0, S0, M0, generation, Operation.TASK_START_N1S1);
  }

  public static Mark taskStart(long generation, long nanoTime, String name, String subName) {
    return new Mark(nanoTime, N0, N0, name, subName, S0, M0, generation, Operation.TASK_START_N1S2);
  }

  public static Mark taskEnd(long generation, long nanoTime, String name) {
    return new Mark(nanoTime, N0, N0, name, S0, S0, M0, generation, Operation.TASK_END_N1S1);
  }

  public static Mark taskEnd(long generation, long nanoTime, String name, String subName) {
    return new Mark(nanoTime, N0, N0, name, subName, S0, M0, generation, Operation.TASK_END_N1S2);
  }

  public static Mark event(long generation, long nanoTime, String name) {
    return new Mark(nanoTime, N0, N0, name, S0, S0, M0, generation, Operation.EVENT_N1S1);
  }

  public static Mark event(long generation, long nanoTime, String name, String subName) {
    return new Mark(nanoTime, N0, N0, name, subName, S0, M0, generation, Operation.EVENT_N1S2);
  }

  public static Mark event(
      long generation, long nanoTime, String taskName, String tagName, long tagId) {
    return new Mark(
        nanoTime, tagId, N0, taskName, tagName, S0, M0, generation, Operation.EVENT_N2S2);
  }

  public static Mark event(
      long generation,
      long nanoTime,
      String taskName,
      String subTaskName,
      String tagName,
      long tagId) {
    return new Mark(
        nanoTime, tagId, N0, taskName, subTaskName, tagName, M0, generation, Operation.EVENT_N2S3);
  }

  public static Mark tag(long generation, String tagName, long tagId) {
    return new Mark(tagId, N0, N0, tagName, S0, S0, M0, generation, Operation.TAG_N1S1);
  }

  public static Mark tag(long generation, long tagId) {
    return new Mark(tagId, N0, N0, S0, S0, S0, M0, generation, Operation.TAG_N1S0);
  }

  public static Mark tag(long generation, String tagName) {
    return new Mark(N0, N0, N0, tagName, S0, S0, M0, generation, Operation.TAG_N0S1);
  }

  public static Mark keyedTag(long generation, String tagName, String value) {
    return new Mark(N0, N0, N0, tagName, value, S0, M0, generation, Operation.TAG_KEYED_N0S2);
  }

  public static Mark keyedTag(long generation, String tagName, long value) {
    return new Mark(value, N0, N0, tagName, S0, S0, M0, generation, Operation.TAG_KEYED_N1S1);
  }

  public static Mark keyedTag(long generation, String tagName, long value0, long value1) {
    return new Mark(value0, value1, N0, tagName, S0, S0, M0, generation, Operation.TAG_KEYED_N2S1);
  }

  public static Mark link(long generation, long linkId) {
    return new Mark(linkId, N0, N0, S0, S0, S0, M0, generation, Operation.LINK);
  }

  private Mark(
      long n1,
      long n2,
      long n3,
      @Nullable String s1,
      @Nullable String s2,
      @Nullable String s3,
      @Nullable Marker m,
      long generation,
      Operation operation) {
    if (operation == null) {
      throw new NullPointerException("operation");
    }
    this.operation = operation;
    this.generation = generation;

    if (operation == Operation.NONE) {
      throw new IllegalArgumentException("bad operation");
    }
    this.n1 = n1;
    this.n2 = n2;
    this.n3 = n3;

    this.s1 = s1;
    this.s2 = s2;
    this.s3 = s3;

    this.m = m;
  }

  public enum OperationType {
    NONE,
    TASK_START,
    TASK_END,
    EVENT,
    MARK,
    LINK,
    TAG,
    ;
  }

  public enum Operation {
    NONE(OperationType.NONE, 0, 0),
    /** startTask(String taskName) 1 long for nanoTime. */
    TASK_START_N1S1(OperationType.TASK_START, 1, 1),
    /** startTask(String name, String subTaskName) 1 long for nanoTime. */
    TASK_START_N1S2(OperationType.TASK_START, 1, 2),

    TASK_END_N1S1(OperationType.TASK_END, 1, 1),
    TASK_END_N1S2(OperationType.TASK_END, 1, 2),

    EVENT_N1S1(OperationType.EVENT, 1, 1),
    EVENT_N1S2(OperationType.EVENT, 1, 2),
    /** Tagged event, since attach tags can't apply to events */
    EVENT_N2S2(OperationType.EVENT, 2, 2),
    /** Tagged event, since attach tags can't apply to events */
    EVENT_N2S3(OperationType.EVENT, 2, 3),

    MARK(OperationType.MARK, 0, 0),

    LINK(OperationType.LINK, 1, 0),

    /** An unkeyed tag that has a single string value. */
    TAG_N0S1(OperationType.TAG, 0, 1),
    /** An unkeyed tag that has a single numeric value. */
    TAG_N1S0(OperationType.TAG, 1, 0),
    /**
     * An unkeyed tag that has a string and numeric value. The values are unrelated to each other.
     */
    TAG_N1S1(OperationType.TAG, 1, 1),

    TAG_KEYED_N1S1(OperationType.TAG, 1, 1),

    TAG_KEYED_N2S1(OperationType.TAG, 2, 1),

    TAG_KEYED_N0S2(OperationType.TAG, 0, 2),
    ;

    private final OperationType opType;
    private final int longs;
    private final int strings;

    Operation(OperationType opType, int longs, int strings) {
      this.opType = opType;
      this.longs = longs;
      this.strings = strings;
      assert longs <= maxNumbers();
      assert strings <= maxStrings();
    }

    private static final Operation[] values = Operation.values();

    static {
      assert values.length <= (1 << Generator.GEN_OFFSET);
    }

    public OperationType getOpType() {
      return opType;
    }

    public int getNumbers() {
      return longs;
    }

    public int getStrings() {
      return strings;
    }

    public int getMarkers() {
      return this == MARK ? 1 : 0;
    }

    public static int maxNumbers() {
      return 2;
    }

    public static int maxStrings() {
      return 3;
    }

    public static int maxMarkers() {
      return 1;
    }

    public static Operation valueOf(int code) {
      return values[code];
    }
  }

  public long getNanoTime() {
    switch (operation.opType) {
      case TASK_START:
      case TASK_END:
      case EVENT:
        return n1;
      case NONE:
      case MARK:
      case LINK:
      case TAG:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  public long getGeneration() {
    return generation;
  }

  public Operation getOperation() {
    return operation;
  }

  public String getTagStringValue() {
    switch (operation) {
      case TAG_N0S1:
      case TAG_N1S1:
        return s1;
      case TAG_KEYED_N0S2:
      case EVENT_N2S2:
        return s2;
      case EVENT_N2S3:
        return s3;
      case TAG_N1S0:
      case NONE:
      case TASK_START_N1S1:
      case TASK_START_N1S2:
      case TASK_END_N1S1:
      case TASK_END_N1S2:
      case EVENT_N1S1:
      case EVENT_N1S2:
      case TAG_KEYED_N1S1:
      case TAG_KEYED_N2S1:
      case MARK:
      case LINK:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  public long getTagFirstNumeric() {
    switch (operation) {
      case TAG_N1S0:
      case TAG_N1S1:
      case TAG_KEYED_N1S1:
      case TAG_KEYED_N2S1:
        return n1;
      case EVENT_N2S2:
      case EVENT_N2S3:
        return n2;
      case TAG_N0S1:
      case TAG_KEYED_N0S2:
      case NONE:
      case TASK_START_N1S1:
      case TASK_START_N1S2:
      case TASK_END_N1S1:
      case TASK_END_N1S2:
      case EVENT_N1S1:
      case EVENT_N1S2:
      case MARK:
      case LINK:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  public long getTagSecondNumeric() {
    switch (operation) {
      case TAG_KEYED_N2S1:
        return n2;
      case TAG_N1S0:
      case TAG_N1S1:
      case TAG_KEYED_N1S1:
      case EVENT_N2S2:
      case EVENT_N2S3:
      case TAG_N0S1:
      case TAG_KEYED_N0S2:
      case NONE:
      case TASK_START_N1S1:
      case TASK_START_N1S2:
      case TASK_END_N1S1:
      case TASK_END_N1S2:
      case EVENT_N1S1:
      case EVENT_N1S2:
      case MARK:
      case LINK:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  public String getTagKey() {
    switch (operation) {
      case TAG_KEYED_N0S2:
      case TAG_KEYED_N1S1:
      case TAG_KEYED_N2S1:
        return s1;
      case TAG_N1S1:
      case TAG_N0S1:
      case TAG_N1S0:
      case NONE:
      case TASK_START_N1S1:
      case TASK_START_N1S2:
      case TASK_END_N1S1:
      case TASK_END_N1S2:
      case EVENT_N1S1:
      case EVENT_N1S2:
      case EVENT_N2S2:
      case EVENT_N2S3:
      case MARK:
      case LINK:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  @Nullable
  public Marker getMarker() {
    switch (operation.opType) {
      case MARK:
        return m;
      case TASK_START:
      case TASK_END:
      case EVENT:
      case NONE:
      case LINK:
      case TAG:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  public String getTaskName() {
    switch (operation) {
      case TASK_START_N1S1:
      case TASK_START_N1S2:
      case TASK_END_N1S1:
      case TASK_END_N1S2:
      case EVENT_N1S1:
      case EVENT_N1S2:
      case EVENT_N2S2:
      case EVENT_N2S3:
        return s1;
      case NONE:
      case MARK:
      case LINK:
      case TAG_N0S1:
      case TAG_N1S0:
      case TAG_N1S1:
      case TAG_KEYED_N0S2:
      case TAG_KEYED_N1S1:
      case TAG_KEYED_N2S1:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation);
  }

  public String getSubTaskName() {
    switch (operation) {
      case TASK_END_N1S2:
      case TASK_START_N1S2:
      case EVENT_N1S2:
      case EVENT_N2S3:
        return s2;
      case TASK_START_N1S1:
      case TASK_END_N1S1:
      case EVENT_N1S1:
      case EVENT_N2S2:
      case NONE:
      case MARK:
      case LINK:
      case TAG_N0S1:
      case TAG_KEYED_N0S2:
      case TAG_KEYED_N1S1:
      case TAG_KEYED_N2S1:
      case TAG_N1S0:
      case TAG_N1S1:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation);
  }

  public long getLinkId() {
    switch (operation.opType) {
      case LINK:
        return n1;
      case MARK:
      case TASK_START:
      case TASK_END:
      case EVENT:
      case NONE:
      case TAG:
        throw new UnsupportedOperationException();
    }
    throw new AssertionError(operation.opType);
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof Mark)) {
      return false;
    }
    Mark that = (Mark) obj;
    return equal(this.s1, that.s1)
        && equal(this.s2, that.s2)
        && equal(this.s3, that.s3)
        && equal(this.m, that.m)
        && this.n1 == that.n1
        && this.n2 == that.n2
        && this.n3 == that.n3
        && this.operation == that.operation
        && this.generation == that.generation;
  }

  @Override
  public int hashCode() {
    return Arrays.hashCode(new Object[] {operation, s1, s2, s3, m, n1, n2, n3, generation});
  }

  @Override
  public String toString() {
    return "Mark{"
        + "operation="
        + operation
        + ", "
        + "s1="
        + s1
        + ", "
        + "s2="
        + s2
        + ", "
        + "s3="
        + s3
        + ", "
        + "m="
        + m
        + ", "
        + "n1="
        + n1
        + ", "
        + "n2="
        + n2
        + ", "
        + "n3="
        + n3
        + ", "
        + "generation="
        + generation
        + "}";
  }

  static <T> boolean equal(T a, T b) {
    return a == b || a.equals(b);
  }
}
