/*
 * Copyright 2022 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 com.google.android.libraries.mobiledatadownload.file;

import android.net.Uri;
import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
import com.google.android.libraries.mobiledatadownload.file.spi.Monitor;
import com.google.android.libraries.mobiledatadownload.file.spi.Transform;
import com.google.common.collect.Iterables;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Encapsulates state for a single open call including selected backend, transforms, etc. This class
 * is used as single parameter to {@link Opener#open} call.
 */
public final class OpenContext {

  private final SynchronousFileStorage storage;
  private final Backend backend;
  private final List<Transform> transforms;
  private final List<Monitor> monitors;
  private final Uri originalUri;
  private final Uri encodedUri;

  /** Builder for constructing an OpenContext. */
  static class Builder {
    private SynchronousFileStorage storage;
    private Backend backend;
    private List<Transform> transforms;
    private List<Monitor> monitors;
    private Uri originalUri;
    private Uri encodedUri;

    private Builder() {}

    Builder setStorage(SynchronousFileStorage storage) {
      this.storage = storage;
      return this;
    }

    Builder setBackend(Backend backend) {
      this.backend = backend;
      return this;
    }

    Builder setTransforms(List<Transform> transforms) {
      this.transforms = transforms;
      return this;
    }

    Builder setMonitors(List<Monitor> monitors) {
      this.monitors = monitors;
      return this;
    }

    Builder setEncodedUri(Uri encodedUri) {
      this.encodedUri = encodedUri;
      return this;
    }

    Builder setOriginalUri(Uri originalUri) {
      this.originalUri = originalUri;
      return this;
    }

    OpenContext build() {
      return new OpenContext(this);
    }
  }

  OpenContext(Builder builder) {
    this.storage = builder.storage;
    this.backend = builder.backend;
    this.transforms = builder.transforms;
    this.monitors = builder.monitors;
    this.originalUri = builder.originalUri;
    this.encodedUri = builder.encodedUri;
  }

  public static OpenContext.Builder builder() {
    return new OpenContext.Builder();
  }

  /** Gets a reference to the same storage instance. */
  public SynchronousFileStorage storage() {
    return storage;
  }

  /** Access the backend selected by the URI. */
  public Backend backend() {
    return backend;
  }

  /**
   * Return the URI after encoding of the filename and stripping of the fragment. This is what the
   * backend sees.
   */
  public Uri encodedUri() {
    return encodedUri;
  }

  /** Get the original URI. This is the one the caller passed to the storage API. */
  public Uri originalUri() {
    return originalUri;
  }

  /**
   * Composes an input stream by chaining {@link MonitorInputStream} and {@link
   * Transform#wrapForRead}s.
   *
   * @return All of the input streams in the chain. The first is returned to client, and the last is
   *     the one produced by the backend.
   */
  public List<InputStream> chainTransformsForRead(InputStream in) throws IOException {
    List<InputStream> chain = new ArrayList<>();
    chain.add(in);
    if (!monitors.isEmpty()) {
      MonitorInputStream monitorStream = MonitorInputStream.newInstance(monitors, originalUri, in);
      if (monitorStream != null) {
        chain.add(monitorStream);
      }
    }
    for (Transform transform : transforms) {
      chain.add(transform.wrapForRead(originalUri, Iterables.getLast(chain)));
    }
    Collections.reverse(chain);
    return chain;
  }

  /**
   * Composes an output stream by chaining {@link MonitorOutputStream} and {@link
   * Transform#wrapForWrite}s.
   *
   * @return All of the output streams in the chain. The first is returned to client, and the last
   *     is the one produced by the backend.
   */
  public List<OutputStream> chainTransformsForWrite(OutputStream out) throws IOException {
    List<OutputStream> chain = new ArrayList<>();
    chain.add(out);
    if (!monitors.isEmpty()) {
      MonitorOutputStream monitorStream =
          MonitorOutputStream.newInstanceForWrite(monitors, originalUri, out);
      if (monitorStream != null) {
        chain.add(monitorStream);
      }
    }
    for (Transform transform : transforms) {
      chain.add(transform.wrapForWrite(originalUri, Iterables.getLast(chain)));
    }
    Collections.reverse(chain);
    return chain;
  }

  /**
   * Composes an output stream by chaining {@link MonitorOutputStream} and {@link
   * Transform#wrapForAppend}s.
   *
   * @return All of the output streams in the chain. The first is returned to client, and the last
   *     is the one produced by the backend.
   */
  public List<OutputStream> chainTransformsForAppend(OutputStream out) throws IOException {
    List<OutputStream> chain = new ArrayList<>();
    chain.add(out);
    if (!monitors.isEmpty()) {
      MonitorOutputStream monitorStream =
          MonitorOutputStream.newInstanceForAppend(monitors, originalUri, out);
      if (monitorStream != null) {
        chain.add(monitorStream);
      }
    }
    for (Transform transform : transforms) {
      chain.add(transform.wrapForAppend(originalUri, Iterables.getLast(chain)));
    }
    Collections.reverse(chain);
    return chain;
  }

  /** Tells whether there are any transforms configured for this open request. */
  public boolean hasTransforms() {
    // NOTE: a more intelligent API might check for any transforms that aren't Sizable
    return !transforms.isEmpty();
  }
}
