/*
 * 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.beam.runners.core;

import com.google.common.base.Splitter;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
import org.apache.beam.sdk.coders.Coder;
import org.apache.beam.sdk.coders.CoderException;
import org.apache.beam.sdk.transforms.windowing.BoundedWindow;
import org.apache.beam.sdk.util.CoderUtils;

/** Factory methods for creating the {@link StateNamespace StateNamespaces}. */
public class StateNamespaces {

  private enum Namespace {
    GLOBAL,
    WINDOW,
    WINDOW_AND_TRIGGER
  }

  public static StateNamespace global() {
    return new GlobalNamespace();
  }

  public static <W extends BoundedWindow> StateNamespace window(Coder<W> windowCoder, W window) {
    return new WindowNamespace<>(windowCoder, window);
  }

  public static <W extends BoundedWindow> StateNamespace windowAndTrigger(
      Coder<W> windowCoder, W window, int triggerIdx) {
    return new WindowAndTriggerNamespace<>(windowCoder, window, triggerIdx);
  }

  private StateNamespaces() {}

  /** {@link StateNamespace} that is global to the current key being processed. */
  public static class GlobalNamespace implements StateNamespace {

    private static final String GLOBAL_STRING = "/";

    @Override
    public String stringKey() {
      return GLOBAL_STRING;
    }

    @Override
    public Object getCacheKey() {
      return GLOBAL_STRING;
    }

    @Override
    public boolean equals(Object obj) {
      return obj == this || obj instanceof GlobalNamespace;
    }

    @Override
    public int hashCode() {
      return Objects.hash(Namespace.GLOBAL);
    }

    @Override
    public String toString() {
      return "Global";
    }

    @Override
    public void appendTo(Appendable sb) throws IOException {
      sb.append(GLOBAL_STRING);
    }
  }

  /** {@link StateNamespace} that is scoped to a specific window. */
  public static class WindowNamespace<W extends BoundedWindow> implements StateNamespace {

    private static final String WINDOW_FORMAT = "/%s/";

    private Coder<W> windowCoder;
    private W window;

    private WindowNamespace(Coder<W> windowCoder, W window) {
      this.windowCoder = windowCoder;
      this.window = window;
    }

    public W getWindow() {
      return window;
    }

    @Override
    public String stringKey() {
      try {
        return String.format(WINDOW_FORMAT, CoderUtils.encodeToBase64(windowCoder, window));
      } catch (CoderException e) {
        throw new RuntimeException("Unable to generate string key from window " + window, e);
      }
    }

    @Override
    public void appendTo(Appendable sb) throws IOException {
      sb.append('/').append(CoderUtils.encodeToBase64(windowCoder, window)).append('/');
    }

    /** State in the same window will all be evicted together. */
    @Override
    public Object getCacheKey() {
      return window;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == this) {
        return true;
      }

      if (!(obj instanceof WindowNamespace)) {
        return false;
      }

      WindowNamespace<?> that = (WindowNamespace<?>) obj;
      return Objects.equals(this.windowStructuralValue(), that.windowStructuralValue());
    }

    private Object windowStructuralValue() {
      return windowCoder.structuralValue(window);
    }

    @Override
    public int hashCode() {
      return Objects.hash(Namespace.WINDOW, window);
    }

    @Override
    public String toString() {
      return "Window(" + window + ")";
    }
  }

  /** {@link StateNamespace} that is scoped to a particular window and trigger index. */
  public static class WindowAndTriggerNamespace<W extends BoundedWindow> implements StateNamespace {

    private static final String WINDOW_AND_TRIGGER_FORMAT = "/%s/%s/";

    private static final int TRIGGER_RADIX = 36;
    private Coder<W> windowCoder;
    private W window;
    private int triggerIndex;

    private WindowAndTriggerNamespace(Coder<W> windowCoder, W window, int triggerIndex) {
      this.windowCoder = windowCoder;
      this.window = window;
      this.triggerIndex = triggerIndex;
    }

    public W getWindow() {
      return window;
    }

    public int getTriggerIndex() {
      return triggerIndex;
    }

    @Override
    public String stringKey() {
      try {
        return String.format(
            WINDOW_AND_TRIGGER_FORMAT,
            CoderUtils.encodeToBase64(windowCoder, window),
            // Use base 36 so that can address 36 triggers in a single byte and still be human
            // readable.
            Integer.toString(triggerIndex, TRIGGER_RADIX).toUpperCase());
      } catch (CoderException e) {
        throw new RuntimeException("Unable to generate string key from window " + window, e);
      }
    }

    @Override
    public void appendTo(Appendable sb) throws IOException {
      sb.append('/').append(CoderUtils.encodeToBase64(windowCoder, window));
      sb.append('/').append(Integer.toString(triggerIndex, TRIGGER_RADIX).toUpperCase());
      sb.append('/');
    }

    /** State in the same window will all be evicted together. */
    @Override
    public Object getCacheKey() {
      return window;
    }

    @Override
    public boolean equals(Object obj) {
      if (obj == this) {
        return true;
      }

      if (!(obj instanceof WindowAndTriggerNamespace)) {
        return false;
      }

      WindowAndTriggerNamespace<?> that = (WindowAndTriggerNamespace<?>) obj;
      return this.triggerIndex == that.triggerIndex
          && Objects.equals(this.windowStructuralValue(), that.windowStructuralValue());
    }

    private Object windowStructuralValue() {
      return windowCoder.structuralValue(window);
    }

    @Override
    public int hashCode() {
      return Objects.hash(Namespace.WINDOW_AND_TRIGGER, window, triggerIndex);
    }

    @Override
    public String toString() {
      return "WindowAndTrigger(" + window + "," + triggerIndex + ")";
    }
  }

  private static final Splitter SLASH_SPLITTER = Splitter.on('/');

  /**
   * Convert a {@code stringKey} produced using {@link StateNamespace#stringKey} on one of the
   * namespaces produced by this class into the original {@link StateNamespace}.
   */
  public static <W extends BoundedWindow> StateNamespace fromString(
      String stringKey, Coder<W> windowCoder) {
    if (!stringKey.startsWith("/") || !stringKey.endsWith("/")) {
      throw new RuntimeException("Invalid namespace string: '" + stringKey + "'");
    }

    if (GlobalNamespace.GLOBAL_STRING.equals(stringKey)) {
      return global();
    }

    List<String> parts = SLASH_SPLITTER.splitToList(stringKey);
    if (parts.size() != 3 && parts.size() != 4) {
      throw new RuntimeException("Invalid namespace string: '" + stringKey + "'");
    }
    // Ends should be empty (we start and end with /)
    if (!parts.get(0).isEmpty() || !parts.get(parts.size() - 1).isEmpty()) {
      throw new RuntimeException("Invalid namespace string: '" + stringKey + "'");
    }

    try {
      W window = CoderUtils.decodeFromBase64(windowCoder, parts.get(1));
      if (parts.size() > 3) {
        int index = Integer.parseInt(parts.get(2), WindowAndTriggerNamespace.TRIGGER_RADIX);
        return windowAndTrigger(windowCoder, window, index);
      } else {
        return window(windowCoder, window);
      }
    } catch (Exception e) {
      throw new RuntimeException("Invalid namespace string: '" + stringKey + "'", e);
    }
  }
}
