// Copyright (C) 2009 The Android Open Source Project
//
// 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.gerrit.server.config;

import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.base.Strings.nullToEmpty;

import com.google.common.flogger.FluentLogger;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.GitwebType;
import com.google.gerrit.common.data.ParameterizedString;
import com.google.gerrit.extensions.common.WebLinkInfo;
import com.google.gerrit.extensions.registration.DynamicSet;
import com.google.gerrit.extensions.restapi.Url;
import com.google.gerrit.extensions.webui.BranchWebLink;
import com.google.gerrit.extensions.webui.FileHistoryWebLink;
import com.google.gerrit.extensions.webui.FileWebLink;
import com.google.gerrit.extensions.webui.ParentWebLink;
import com.google.gerrit.extensions.webui.PatchSetWebLink;
import com.google.gerrit.extensions.webui.ProjectWebLink;
import com.google.gerrit.extensions.webui.TagWebLink;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.net.MalformedURLException;
import java.net.URL;
import org.eclipse.jgit.lib.Config;

public class GitwebConfig {
  private static final FluentLogger logger = FluentLogger.forEnclosingClass();

  public static boolean isDisabled(Config cfg) {
    return isEmptyString(cfg, "gitweb", null, "url")
        || isEmptyString(cfg, "gitweb", null, "cgi")
        || "disabled".equals(cfg.getString("gitweb", null, "type"));
  }

  public static class LegacyModule extends AbstractModule {
    private final Config cfg;

    public LegacyModule(Config cfg) {
      this.cfg = cfg;
    }

    @Override
    protected void configure() {
      GitwebType type = typeFromConfig(cfg);
      if (type != null) {
        bind(GitwebType.class).toInstance(type);

        if (!isNullOrEmpty(type.getBranch())) {
          DynamicSet.bind(binder(), BranchWebLink.class).to(GitwebLinks.class);
        }

        if (!isNullOrEmpty(type.getTag())) {
          DynamicSet.bind(binder(), TagWebLink.class).to(GitwebLinks.class);
        }

        if (!isNullOrEmpty(type.getFile()) || !isNullOrEmpty(type.getRootTree())) {
          DynamicSet.bind(binder(), FileWebLink.class).to(GitwebLinks.class);
        }

        if (!isNullOrEmpty(type.getFileHistory())) {
          DynamicSet.bind(binder(), FileHistoryWebLink.class).to(GitwebLinks.class);
        }

        if (!isNullOrEmpty(type.getRevision())) {
          DynamicSet.bind(binder(), PatchSetWebLink.class).to(GitwebLinks.class);
          DynamicSet.bind(binder(), ParentWebLink.class).to(GitwebLinks.class);
        }

        if (!isNullOrEmpty(type.getProject())) {
          DynamicSet.bind(binder(), ProjectWebLink.class).to(GitwebLinks.class);
        }
      }
    }
  }

  private static boolean isEmptyString(Config cfg, String section, String subsection, String name) {
    // This is currently the only way to check for the empty string in a JGit
    // config. Fun!
    String[] values = cfg.getStringList(section, subsection, name);
    return values.length > 0 && isNullOrEmpty(values[0]);
  }

  private static GitwebType typeFromConfig(Config cfg) {
    GitwebType defaultType = defaultType(cfg.getString("gitweb", null, "type"));
    if (defaultType == null) {
      return null;
    }
    GitwebType type = new GitwebType();

    type.setLinkName(
        firstNonNull(cfg.getString("gitweb", null, "linkname"), defaultType.getLinkName()));
    type.setBranch(firstNonNull(cfg.getString("gitweb", null, "branch"), defaultType.getBranch()));
    type.setTag(firstNonNull(cfg.getString("gitweb", null, "tag"), defaultType.getTag()));
    type.setProject(
        firstNonNull(cfg.getString("gitweb", null, "project"), defaultType.getProject()));
    type.setRevision(
        firstNonNull(cfg.getString("gitweb", null, "revision"), defaultType.getRevision()));
    type.setRootTree(
        firstNonNull(cfg.getString("gitweb", null, "roottree"), defaultType.getRootTree()));
    type.setFile(firstNonNull(cfg.getString("gitweb", null, "file"), defaultType.getFile()));
    type.setFileHistory(
        firstNonNull(cfg.getString("gitweb", null, "filehistory"), defaultType.getFileHistory()));
    type.setUrlEncode(cfg.getBoolean("gitweb", null, "urlencode", defaultType.getUrlEncode()));
    String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
    if (pathSeparator != null) {
      if (pathSeparator.length() == 1) {
        char c = pathSeparator.charAt(0);
        if (isValidPathSeparator(c)) {
          type.setPathSeparator(firstNonNull(c, defaultType.getPathSeparator()));
        } else {
          logger.atWarning().log("Invalid gitweb.pathSeparator: %s", c);
        }
      } else {
        logger.atWarning().log("gitweb.pathSeparator is not a single character: %s", pathSeparator);
      }
    }
    return type;
  }

  private static GitwebType defaultType(String typeName) {
    GitwebType type = new GitwebType();
    switch (nullToEmpty(typeName)) {
      case "gitweb":
        type.setLinkName("gitweb");
        type.setProject("?p=${project}.git;a=summary");
        type.setRevision("?p=${project}.git;a=commit;h=${commit}");
        type.setBranch("?p=${project}.git;a=shortlog;h=${branch}");
        type.setTag("?p=${project}.git;a=tag;h=${tag}");
        type.setRootTree("?p=${project}.git;a=tree;hb=${commit}");
        type.setFile("?p=${project}.git;hb=${commit};f=${file}");
        type.setFileHistory("?p=${project}.git;a=history;hb=${branch};f=${file}");
        break;
      case "cgit":
        type.setLinkName("cgit");
        type.setProject("${project}.git/summary");
        type.setRevision("${project}.git/commit/?id=${commit}");
        type.setBranch("${project}.git/log/?h=${branch}");
        type.setTag("${project}.git/tag/?h=${tag}");
        type.setRootTree("${project}.git/tree/?h=${commit}");
        type.setFile("${project}.git/tree/${file}?h=${commit}");
        type.setFileHistory("${project}.git/log/${file}?h=${branch}");
        break;
      case "custom":
        // For a custom type with no explicit link name, just reuse "gitweb".
        type.setLinkName("gitweb");
        type.setProject("");
        type.setRevision("");
        type.setBranch("");
        type.setTag("");
        type.setRootTree("");
        type.setFile("");
        type.setFileHistory("");
        break;
      case "":
      case "disabled":
      default:
        return null;
    }
    return type;
  }

  private final String url;
  private final GitwebType type;

  @Inject
  GitwebConfig(
      GitwebCgiConfig cgiConfig,
      @GerritServerConfig Config cfg,
      @Nullable @CanonicalWebUrl String gerritUrl)
      throws MalformedURLException {
    if (isDisabled(cfg)) {
      type = null;
      url = null;
    } else {
      String cfgUrl = cfg.getString("gitweb", null, "url");
      type = typeFromConfig(cfg);
      if (type == null) {
        url = null;
      } else if (cgiConfig.getGitwebCgi() == null) {
        // Use an externally managed gitweb instance, and not an internal one.
        url = cfgUrl;
      } else {
        String baseGerritUrl;
        if (gerritUrl != null) {
          URL u = new URL(gerritUrl);
          baseGerritUrl = u.getPath();
        } else {
          baseGerritUrl = "/";
        }
        url = firstNonNull(cfgUrl, baseGerritUrl + "gitweb");
      }
    }
  }

  /** @return GitwebType for gitweb viewer. */
  @Nullable
  public GitwebType getGitwebType() {
    return type;
  }

  /**
   * @return URL of the entry point into gitweb. This URL may be relative to our context if gitweb
   *     is hosted by ourselves; or absolute if its hosted elsewhere; or null if gitweb has not been
   *     configured.
   */
  public String getUrl() {
    return url;
  }

  /**
   * Determines if a given character can be used unencoded in an URL as a replacement for the path
   * separator '/'.
   *
   * <p>Reasoning: http://www.ietf.org/rfc/rfc1738.txt § 2.2:
   *
   * <p>... only alphanumerics, the special characters "$-_.+!*'(),", and reserved characters used
   * for their reserved purposes may be used unencoded within a URL.
   *
   * <p>The following characters might occur in file names, however:
   *
   * <p>alphanumeric characters,
   *
   * <p>"$-_.+!',"
   */
  static boolean isValidPathSeparator(char c) {
    switch (c) {
      case '*':
      case '(':
      case ')':
        return true;
      default:
        return false;
    }
  }

  @Singleton
  static class GitwebLinks
      implements BranchWebLink,
          FileHistoryWebLink,
          FileWebLink,
          PatchSetWebLink,
          ParentWebLink,
          ProjectWebLink,
          TagWebLink {
    private final String url;
    private final GitwebType type;
    private final ParameterizedString branch;
    private final ParameterizedString file;
    private final ParameterizedString fileHistory;
    private final ParameterizedString project;
    private final ParameterizedString revision;
    private final ParameterizedString tag;

    @Inject
    GitwebLinks(GitwebConfig config, GitwebType type) {
      this.url = config.getUrl();
      this.type = type;
      this.branch = parse(type.getBranch());
      this.file = parse(firstNonNull(emptyToNull(type.getFile()), nullToEmpty(type.getRootTree())));
      this.fileHistory = parse(type.getFileHistory());
      this.project = parse(type.getProject());
      this.revision = parse(type.getRevision());
      this.tag = parse(type.getTag());
    }

    @Override
    public WebLinkInfo getBranchWebLink(String projectName, String branchName) {
      if (branch != null) {
        return link(
            branch
                .replace("project", encode(projectName))
                .replace("branch", encode(branchName))
                .toString());
      }
      return null;
    }

    @Override
    public WebLinkInfo getTagWebLink(String projectName, String tagName) {
      if (tag != null) {
        return link(
            tag.replace("project", encode(projectName)).replace("tag", encode(tagName)).toString());
      }
      return null;
    }

    @Override
    public WebLinkInfo getFileHistoryWebLink(String projectName, String revision, String fileName) {
      if (fileHistory != null) {
        return link(
            fileHistory
                .replace("project", encode(projectName))
                .replace("branch", encode(revision))
                .replace("file", encode(fileName))
                .toString());
      }
      return null;
    }

    @Override
    public WebLinkInfo getFileWebLink(String projectName, String revision, String fileName) {
      if (file != null) {
        return link(
            file.replace("project", encode(projectName))
                .replace("commit", encode(revision))
                .replace("file", encode(fileName))
                .toString());
      }
      return null;
    }

    @Override
    public WebLinkInfo getPatchSetWebLink(
        String projectName, String commit, String commitMessage, String branchName) {
      if (revision != null) {
        // commitMessage and branchName are not needed, hence not used.
        return link(
            revision
                .replace("project", encode(projectName))
                .replace("commit", encode(commit))
                .toString());
      }
      return null;
    }

    @Override
    public WebLinkInfo getParentWebLink(
        String projectName, String commit, String commitMessage, String branchName) {
      // For Gitweb treat parent revision links the same as patch set links
      return getPatchSetWebLink(projectName, commit, commitMessage, branchName);
    }

    @Override
    public WebLinkInfo getProjectWeblink(String projectName) {
      if (project != null) {
        return link(project.replace("project", encode(projectName)).toString());
      }
      return null;
    }

    private String encode(String val) {
      if (type.getUrlEncode()) {
        return Url.encode(type.replacePathSeparator(val));
      }
      return val;
    }

    private WebLinkInfo link(String rest) {
      return new WebLinkInfo(type.getLinkName(), null, url + rest, null);
    }

    private static ParameterizedString parse(String pattern) {
      if (!isNullOrEmpty(pattern)) {
        return new ParameterizedString(pattern);
      }
      return null;
    }
  }
}
