// Copyright 2017 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.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.devtools.build.android.FullyQualifiedName.Factory;
import com.google.devtools.build.android.ParsedAndroidData.CombiningConsumer;
import com.google.devtools.build.android.ParsedAndroidData.KeyValueConsumer;
import com.google.devtools.build.android.ParsedAndroidData.OverwritableConsumer;
import com.google.devtools.build.android.resources.Visibility;
import com.google.devtools.build.android.xml.Namespaces;
import com.google.errorprone.annotations.CanIgnoreReturnValue;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;

/**
 * Build for ParsedAndroidData instance.
 */
public class ParsedAndroidDataBuilder {

  private final Path defaultRoot;
  private final FullyQualifiedName.Factory fqnFactory;
  private final Map<DataKey, DataResource> overwrite = new HashMap<>();
  private final Map<DataKey, DataResource> combine = new HashMap<>();
  private final Map<DataKey, DataAsset> assets = new HashMap<>();
  private final Set<MergeConflict> conflicts = new HashSet<>();

  public ParsedAndroidDataBuilder(
      @Nullable Path root, @Nullable FullyQualifiedName.Factory fqnFactory) {
    this.defaultRoot = root;
    this.fqnFactory = fqnFactory;
  }

  public static ParsedAndroidData empty() {
    return ParsedAndroidData.of(
        ImmutableSet.<MergeConflict>of(),
        ImmutableMap.<DataKey, DataResource>of(),
        ImmutableMap.<DataKey, DataResource>of(),
        ImmutableMap.<DataKey, DataAsset>of());
  }

  public static ParsedAndroidDataBuilder buildOn(
      Path defaultRoot, FullyQualifiedName.Factory fqnFactory) {
    return new ParsedAndroidDataBuilder(defaultRoot, fqnFactory);
  }

  public static ParsedAndroidDataBuilder buildOn(FullyQualifiedName.Factory fqnFactory) {
    return buildOn(null, fqnFactory);
  }

  public static ParsedAndroidDataBuilder buildOn(Path defaultRoot) {
    return buildOn(defaultRoot, null);
  }

  public static ParsedAndroidDataBuilder builder() {
    return buildOn(null, null);
  }

  @CanIgnoreReturnValue
  public ParsedAndroidDataBuilder overwritable(DataEntry... resourceBuilders) {
    OverwritableConsumer<DataKey, DataResource> consumer =
        new OverwritableConsumer<>(overwrite, conflicts);
    for (DataEntry resourceBuilder : resourceBuilders) {
      resourceBuilder.accept(fqnFactory, defaultRoot, consumer);
    }
    return this;
  }

  @CanIgnoreReturnValue
  public ParsedAndroidDataBuilder combining(DataEntry... resourceBuilders) {
    CombiningConsumer consumer = new CombiningConsumer(combine);
    for (DataEntry resourceBuilder : resourceBuilders) {
      resourceBuilder.accept(fqnFactory, defaultRoot, consumer);
    }
    return this;
  }

  @CanIgnoreReturnValue
  public ParsedAndroidDataBuilder assets(DataEntry... assetBuilders) {
    OverwritableConsumer<DataKey, DataAsset> consumer =
        new OverwritableConsumer<>(assets, conflicts);
    for (DataEntry assetBuilder : assetBuilders) {
      assetBuilder.accept(defaultRoot, consumer);
    }
    return this;
  }

  public static FileResourceBuilder file(String rawKey) {
    return new FileResourceBuilder(rawKey);
  }

  public static FileResourceBuilder file() {
    return new FileResourceBuilder(null);
  }

  public static XmlResourceBuilder xml(String rawKey) {
    return new XmlResourceBuilder(rawKey);
  }

  public ParsedAndroidData build() {
    return ParsedAndroidData.of(
        ImmutableSet.copyOf(conflicts),
        ImmutableMap.copyOf(overwrite),
        ImmutableMap.copyOf(combine),
        ImmutableMap.copyOf(assets));
  }

  static class FileResourceBuilder {
    private String rawKey;
    private Path root;

    FileResourceBuilder(@Nullable String rawKey) {
      this.rawKey = rawKey;
    }

    @CanIgnoreReturnValue
    FileResourceBuilder root(Path root) {
      this.root = root;
      return this;
    }

    Path chooseRoot(Path defaultRoot) {
      if (defaultRoot != null) {
        return defaultRoot;
      }
      if (root != null) {
        return root;
      }
      throw new IllegalStateException(
          "the default root and asset root are null! A root is required!");
    }

    DataEntry source(final DataSource source) {
      return new DataEntry() {
        @Override
        void accept(
            @Nullable Factory factory,
            @Nullable Path root,
            KeyValueConsumer<DataKey, DataResource> consumer) {
          consumer.accept(
              factory.parse(rawKey),
              DataValueFile.of(
                  Visibility.UNKNOWN, source, /*fingerprint=*/ null, /*rootXmlNode=*/ null));
        }

        @Override
        void accept(@Nullable Path defaultRoot, KeyValueConsumer<DataKey, DataAsset> target) {
          target.accept(
              RelativeAssetPath.Factory.of(chooseRoot(defaultRoot).resolve("assets"))
                  .create(source.getPath()),
              DataValueFile.of(
                  Visibility.UNKNOWN, source, /*fingerprint=*/ null, /*rootXmlNode=*/ null));
        }
      };
    }

    DataEntry source(final String path) {
      return new DataEntry() {
        @Override
        public void accept(
            FullyQualifiedName.Factory factory,
            Path defaultRoot,
            KeyValueConsumer<DataKey, DataResource> consumer) {
          Path res = chooseRoot(defaultRoot).resolve("res");
          consumer.accept(factory.parse(rawKey), DataValueFile.of(res.resolve(path)));
        }

        @Override
        public void accept(
            @Nullable Path defaultRoot, KeyValueConsumer<DataKey, DataAsset> consumer) {
          Path assets = chooseRoot(defaultRoot).resolve("assets");
          Path fullPath = assets.resolve(path);
          consumer.accept(
              RelativeAssetPath.Factory.of(assets).create(fullPath), DataValueFile.of(fullPath));
        }
      };
    }
  }

  static class XmlResourceBuilder {
    private final String rawFqn;
    private Path root;
    private final Map<String, String> prefixToUri = new LinkedHashMap<>();

    XmlResourceBuilder(String rawFqn) {
      this(rawFqn, null);
    }

    XmlResourceBuilder(String rawFqn, Path root) {
      this.rawFqn = rawFqn;
      this.root = root;
    }

    XmlResourceBuilder source(final String path) {
      return new XmlResourceBuilder(rawFqn, root) {
        @Override
        public DataEntry value(final XmlResourceValue value) {
          return new DataEntry() {
            @Override
            public void accept(
                FullyQualifiedName.Factory factory,
                Path defaultRoot,
                KeyValueConsumer<DataKey, DataResource> consumer) {
              Path res = (root == null ? defaultRoot : root).resolve("res");
              consumer.accept(
                  factory.parse(rawFqn),
                  DataResourceXml.createWithNamespaces(
                      res.resolve(path), value, Namespaces.from(prefixToUri)));
            }
          };
        }
      };
    }

    XmlResourceBuilder source(final DataSource dataSource) {
      return new XmlResourceBuilder(rawFqn, root) {
        @Override
        public DataEntry value(final XmlResourceValue value) {
          return new DataEntry() {
            @Override
            public void accept(
                FullyQualifiedName.Factory factory,
                Path defaultRoot,
                KeyValueConsumer<DataKey, DataResource> consumer) {
              consumer.accept(
                  factory.parse(rawFqn),
                  DataResourceXml.createWithNamespaces(
                      dataSource, value, Namespaces.from(prefixToUri)));
            }
          };
        }
      };
    }

    @CanIgnoreReturnValue
    XmlResourceBuilder root(Path root) {
      this.root = root;
      return this;
    }

    @CanIgnoreReturnValue
    XmlResourceBuilder namespace(String prefix, String uri) {
      prefixToUri.put(prefix, uri);
      return this;
    }

    DataEntry value(final XmlResourceValue value) {
      throw new UnsupportedOperationException("A source must be declared!");
    }
  }

  abstract static class DataEntry {
    void accept(
        @Nullable FullyQualifiedName.Factory factory,
        @Nullable Path root,
        KeyValueConsumer<DataKey, DataResource> consumer) {
      throw new UnsupportedOperationException("assets cannot be resources!");
    }

    void accept(@Nullable Path root, KeyValueConsumer<DataKey, DataAsset> target) {
      throw new UnsupportedOperationException("xml resources cannot be assets!");
    }
  }
}
