// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.android;

import com.google.common.base.Function;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.android.proto.SerializeFormat;
import com.google.devtools.build.android.proto.SerializeFormat.Header;
import com.google.devtools.build.android.proto.SerializeFormat.ProtoSource;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileSystem;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.NavigableMap;

/**
 * Tracks mappings from resource source paths (/foo/bar/res/values/colors.xml) to an ID for a more
 * compact serialization format.
 */
class DataSourceTable {

  private static final Function<DataValue, DataSource> VALUE_TO_SOURCE =
      new Function<DataValue, DataSource>() {
        @Override
        public DataSource apply(DataValue input) {
          return input.source();
        }
      };
  private final Map<DataSource, Integer> sourceTable = new LinkedHashMap<>();
  private DataSource[] idToSource;

  /**
   * Creates a DataSourceTable and serialize to the given outstream. Assigns each resource source
   * path a number to enable {@link #getSourceId(DataSource)} queries.
   *
   * @param map the final map of resources
   * @param outStream stream to serialize the source table
   * @param headerBuilder the header to serialize
   * @throws IOException if this fails to serialize the table to the outStream
   */
  public static DataSourceTable createAndWrite(
      NavigableMap<DataKey, DataValue> map, OutputStream outStream, Header.Builder headerBuilder)
      throws IOException {
    DataSourceTable sourceTable = new DataSourceTable();
    sourceTable.writeSourceInfo(map, outStream);
    sourceTable.setHeader(headerBuilder);
    return sourceTable;
  }

  /** Convert the absolute source path to the source table index */
  public int getSourceId(DataSource source) {
    return sourceTable.get(source);
  }

  private void writeSourceInfo(NavigableMap<DataKey, DataValue> map, OutputStream outStream)
      throws IOException {
    int sourceNumber = 0;
    LinkedList<DataSource> sourceQueue =
        new LinkedList<>(Collections2.transform(map.values(), VALUE_TO_SOURCE));
    while (!sourceQueue.isEmpty()) {
      DataSource source = sourceQueue.pop();
      if (!sourceTable.containsKey(source)) {
        sourceTable.put(source, sourceNumber);
        ++sourceNumber;
        sourceQueue.addAll(source.overrides());
      }
    }
    for (DataSource dataSource : sourceTable.keySet()) {
      ProtoSource.newBuilder()
          .setFilename(dataSource.getPath().toString())
          .addAllOverwritten(sourcesToIds(dataSource.overrides()))
          .build()
          .writeDelimitedTo(outStream);
    }
  }

  private List<Integer> sourcesToIds(ImmutableSet<DataSource> overrides) {
    ImmutableList.Builder<Integer> idsBuilder = ImmutableList.builder();
    for (DataSource dataSource : overrides) {
      if (!sourceTable.containsKey(dataSource)) {
        throw new IllegalArgumentException(
            "Cannot find data source: " + dataSource.toString() + " in " + sourceTable.keySet());
      }
      idsBuilder.add(sourceTable.get(dataSource));
    }
    return idsBuilder.build();
  }

  /** Fill in the serialize format header information required to deserialize */
  private Header.Builder setHeader(Header.Builder headerBuilder) {
    return headerBuilder.setSourceCount(sourceTable.size());
  }

  /** Deserialize the source table and allow {@link #sourceFromId(int)} queries. */
  public static DataSourceTable read(InputStream in, FileSystem currentFileSystem, Header header)
      throws IOException {
    DataSourceTable sourceTable = new DataSourceTable();
    sourceTable.readSourceInfo(in, currentFileSystem, header);
    return sourceTable;
  }

  /** Convert the source ID to full Path */
  public DataSource sourceFromId(int sourceId) {
    return idToSource[sourceId];
  }

  private void readSourceInfo(InputStream in, FileSystem currentFileSystem, Header header)
      throws IOException {
    int numberOfSources = header.getSourceCount();
    // Read back the sources.
    idToSource = new DataSource[numberOfSources];
    for (int i = 0; i < numberOfSources; i++) {
      ProtoSource protoSource = SerializeFormat.ProtoSource.parseDelimitedFrom(in);
      idToSource[i] = DataSource.from(protoSource, currentFileSystem);
    }
  }
}
