// Copyright (C) 2014 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.sshd.commands;

import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.PatchSet;
import com.google.gerrit.entities.Project;
import com.google.gerrit.git.ObjectIds;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.change.ChangeFinder;
import com.google.gerrit.server.notedb.ChangeNotes;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.sshd.BaseCommand.UnloggedFailure;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Singleton
public class PatchSetParser {
  private final Provider<InternalChangeQuery> queryProvider;
  private final ChangeNotes.Factory notesFactory;
  private final PatchSetUtil psUtil;
  private final ChangeFinder changeFinder;

  @Inject
  PatchSetParser(
      Provider<InternalChangeQuery> queryProvider,
      ChangeNotes.Factory notesFactory,
      PatchSetUtil psUtil,
      ChangeFinder changeFinder) {
    this.queryProvider = queryProvider;
    this.notesFactory = notesFactory;
    this.psUtil = psUtil;
    this.changeFinder = changeFinder;
  }

  public PatchSet parsePatchSet(String token, ProjectState projectState, String branch)
      throws UnloggedFailure {
    // By commit?
    //
    if (token.matches("^([0-9a-fA-F]{4," + ObjectIds.STR_LEN + "})$")) {
      InternalChangeQuery query = queryProvider.get();
      List<ChangeData> cds;
      if (projectState != null) {
        Project.NameKey p = projectState.getNameKey();
        if (branch != null) {
          cds = query.byBranchCommit(p.get(), branch, token);
        } else {
          cds = query.byProjectCommit(p, token);
        }
      } else {
        cds = query.byCommit(token);
      }
      List<PatchSet> matches = new ArrayList<>(cds.size());
      for (ChangeData cd : cds) {
        Change c = cd.change();
        if (!(inProject(c, projectState) && inBranch(c, branch))) {
          continue;
        }
        for (PatchSet ps : cd.patchSets()) {
          if (ObjectIds.matchesAbbreviation(ps.commitId(), token)) {
            matches.add(ps);
          }
        }
      }

      switch (matches.size()) {
        case 1:
          return matches.iterator().next();
        case 0:
          throw error("\"" + token + "\" no such patch set");
        default:
          throw error("\"" + token + "\" matches multiple patch sets");
      }
    }

    // By older style change,patchset?
    //
    if (token.matches("^[1-9][0-9]*,[1-9][0-9]*$")) {
      PatchSet.Id patchSetId;
      try {
        patchSetId = PatchSet.Id.parse(token);
      } catch (IllegalArgumentException e) {
        throw error("\"" + token + "\" is not a valid patch set", e);
      }
      ChangeNotes notes = getNotes(projectState, patchSetId.changeId());
      PatchSet patchSet = psUtil.get(notes, patchSetId);
      if (patchSet == null) {
        throw error("\"" + token + "\" no such patch set");
      }
      if (projectState != null || branch != null) {
        Change change = notes.getChange();
        if (!inProject(change, projectState)) {
          throw error("change " + change.getId() + " not in project " + projectState.getName());
        }
        if (!inBranch(change, branch)) {
          throw error("change " + change.getId() + " not in branch " + branch);
        }
      }
      return patchSet;
    }

    throw error("\"" + token + "\" is not a valid patch set");
  }

  private ChangeNotes getNotes(@Nullable ProjectState projectState, Change.Id changeId)
      throws UnloggedFailure {
    if (projectState != null) {
      return notesFactory.create(projectState.getNameKey(), changeId);
    }
    Optional<ChangeNotes> notes = changeFinder.findOne(changeId);
    if (!notes.isPresent()) {
      throw error("\"" + changeId + "\" no such change");
    }
    return notesFactory.create(notes.get().getProjectName(), changeId);
  }

  private static boolean inProject(Change change, ProjectState projectState) {
    if (projectState == null) {
      // No --project option, so they want every project.
      return true;
    }
    return projectState.getNameKey().equals(change.getProject());
  }

  private static boolean inBranch(Change change, String branch) {
    if (branch == null) {
      // No --branch option, so they want every branch.
      return true;
    }
    return change.getDest().branch().equals(branch);
  }

  public static UnloggedFailure error(String msg) {
    return new UnloggedFailure(1, msg);
  }

  public static UnloggedFailure error(String msg, Throwable why) {
    return new UnloggedFailure(1, msg, why);
  }
}
