/*
 * Copyright 2014 gitblit.com.
 *
 * 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.gitblit.wicket.pages;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.apache.wicket.behavior.SimpleAttributeModifier;
import org.apache.wicket.markup.html.form.Button;
import org.apache.wicket.markup.html.form.Form;
import org.apache.wicket.model.CompoundPropertyModel;
import org.apache.wicket.model.IModel;
import org.apache.wicket.model.Model;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.RefUpdate.Result;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;

import com.gitblit.Constants;
import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.GitBlitException;
import com.gitblit.Keys;
import com.gitblit.models.RepositoryModel;
import com.gitblit.models.UserModel;
import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.FileUtils;
import com.gitblit.utils.StringUtils;
import com.gitblit.wicket.GitBlitWebSession;
import com.gitblit.wicket.WicketUtils;
import com.gitblit.wicket.panels.AccessPolicyPanel;
import com.gitblit.wicket.panels.BooleanChoiceOption;
import com.gitblit.wicket.panels.BooleanOption;
import com.gitblit.wicket.panels.RepositoryNamePanel;
import com.google.common.base.Optional;

public class NewRepositoryPage extends RootSubPage {

	private final RepositoryModel repositoryModel;
	private IModel<Boolean> addReadmeModel;
	private Model<String> gitignoreModel;
	private IModel<Boolean> addGitflowModel;
	private IModel<Boolean> addGitignoreModel;
	private AccessPolicyPanel accessPolicyPanel;
	private RepositoryNamePanel namePanel;

	public NewRepositoryPage() {
		// create constructor
		super();
		repositoryModel = new RepositoryModel();

		setupPage(getString("gb.newRepository"), "");

		setStatelessHint(false);
		setOutputMarkupId(true);
	}

	@Override
	protected boolean requiresPageMap() {
		return true;
	}

	@Override
	protected Class<? extends BasePage> getRootNavPageClass() {
		return RepositoriesPage.class;
	}

	@Override
	protected void onInitialize() {
		super.onInitialize();

		CompoundPropertyModel<RepositoryModel> rModel = new CompoundPropertyModel<>(repositoryModel);
		Form<RepositoryModel> form = new Form<RepositoryModel>("editForm", rModel) {

			private static final long serialVersionUID = 1L;

			@Override
			protected void onSubmit() {
				try {
					if (!namePanel.updateModel(repositoryModel)) {
						return;
					}
					accessPolicyPanel.updateModel(repositoryModel);

					repositoryModel.owners = new ArrayList<String>();
					repositoryModel.owners.add(GitBlitWebSession.get().getUsername());

					// setup branch defaults
					boolean useGitFlow = addGitflowModel.getObject();

					repositoryModel.HEAD = Constants.R_MASTER;
					repositoryModel.mergeTo = Constants.MASTER;
					if (useGitFlow) {
						// tickets normally merge to develop unless they are hotfixes
						repositoryModel.mergeTo = Constants.DEVELOP;
					}

					repositoryModel.allowForks = app().settings().getBoolean(Keys.web.allowForking, true);

					// optionally generate an initial commit
					boolean addReadme = addReadmeModel.getObject();
					String gitignore = null;
					boolean addGitignore = addGitignoreModel.getObject();
					if (addGitignore) {
						gitignore = gitignoreModel.getObject();
						if (StringUtils.isEmpty(gitignore)) {
							throw new GitBlitException(getString("gb.pleaseSelectGitIgnore"));
						}
					}

					// init the repository
					app().gitblit().updateRepositoryModel(repositoryModel.name, repositoryModel, true);

					// optionally create an initial commit
					initialCommit(repositoryModel, addReadme, gitignore, useGitFlow);

				} catch (GitBlitException e) {
					error(e.getMessage());
					return;
				}
				setRedirect(true);
				setResponsePage(SummaryPage.class, WicketUtils.newRepositoryParameter(repositoryModel.name));
			}
		};

		// do not let the browser pre-populate these fields
		form.add(new SimpleAttributeModifier("autocomplete", "off"));

		namePanel = new RepositoryNamePanel("namePanel", repositoryModel);
		form.add(namePanel);

		// prepare the default access controls
		AccessRestrictionType defaultRestriction = AccessRestrictionType.fromName(
				app().settings().getString(Keys.git.defaultAccessRestriction, AccessRestrictionType.PUSH.name()));
		if (AccessRestrictionType.NONE == defaultRestriction) {
			defaultRestriction = AccessRestrictionType.PUSH;
		}
		AuthorizationControl defaultControl = AuthorizationControl.fromName(
				app().settings().getString(Keys.git.defaultAuthorizationControl, AuthorizationControl.NAMED.name()));

		if (AuthorizationControl.AUTHENTICATED == defaultControl) {
			defaultRestriction = AccessRestrictionType.PUSH;
		}

		repositoryModel.authorizationControl = defaultControl;
		repositoryModel.accessRestriction = defaultRestriction;

		accessPolicyPanel = new AccessPolicyPanel("accessPolicyPanel", repositoryModel);
		form.add(accessPolicyPanel);

		//
		// initial commit options
		//

		// add README
		addReadmeModel = Model.of(false);
		form.add(new BooleanOption("addReadme",
				getString("gb.initWithReadme"),
				getString("gb.initWithReadmeDescription"),
				addReadmeModel));

		// add .gitignore
		File gitignoreDir = app().runtime().getFileOrFolder(Keys.git.gitignoreFolder, "${baseFolder}/gitignore");
		File [] files = gitignoreDir.listFiles();
		if (files == null) {
			files = new File[0];
		}
		List<String> gitignores = new ArrayList<String>();
		for (File file : files) {
			if (file.isFile() && file.getName().endsWith(".gitignore")) {
				gitignores.add(StringUtils.stripFileExtension(file.getName()));
			}
		}
		Collections.sort(gitignores);

		gitignoreModel = Model.of("");
		addGitignoreModel = Model.of(false);
		form.add(new BooleanChoiceOption<String>("addGitIgnore",
				getString("gb.initWithGitignore"),
				getString("gb.initWithGitignoreDescription"),
				addGitignoreModel,
				gitignoreModel,
				gitignores).setVisible(gitignores.size() > 0));

		// TODO consider gitflow at creation (ticket-55)
		addGitflowModel = Model.of(false);
		form.add(new BooleanOption("addGitFlow",
				"Include a .gitflow file",
				"This will generate a config file which guides Git clients in setting up Gitflow branches.",
				addGitflowModel).setVisible(false));

		form.add(new Button("create"));

		add(form);
	}

	/**
	 * Prepare the initial commit for the repository.
	 *
	 * @param repository
	 * @param addReadme
	 * @param gitignore
	 * @param addGitFlow
	 * @return true if an initial commit was created
	 */
	protected boolean initialCommit(RepositoryModel repository, boolean addReadme, String gitignore,
			boolean addGitFlow) {
		boolean initialCommit = addReadme || !StringUtils.isEmpty(gitignore) || addGitFlow;
		if (!initialCommit) {
			return false;
		}

		// build an initial commit
		boolean success = false;
		Repository db = app().repositories().getRepository(repositoryModel.name);
		ObjectInserter odi = db.newObjectInserter();
		try {

			UserModel user = GitBlitWebSession.get().getUser();
			String email = Optional.fromNullable(user.emailAddress).or(user.username + "@" + "gitblit");
			PersonIdent author = new PersonIdent(user.getDisplayName(), email);

			DirCache newIndex = DirCache.newInCore();
			DirCacheBuilder indexBuilder = newIndex.builder();

			if (addReadme) {
				// insert a README
				String title = StringUtils.stripDotGit(StringUtils.getLastPathElement(repositoryModel.name));
				String description = repositoryModel.description == null ? "" : repositoryModel.description;
				String readme = String.format("## %s\n\n%s\n\n", title, description);
				byte [] bytes = readme.getBytes(Constants.ENCODING);

				DirCacheEntry entry = new DirCacheEntry("README.md");
				entry.setLength(bytes.length);
				entry.setLastModified(System.currentTimeMillis());
				entry.setFileMode(FileMode.REGULAR_FILE);
				entry.setObjectId(odi.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes));

				indexBuilder.add(entry);
			}

			if (!StringUtils.isEmpty(gitignore)) {
				// insert a .gitignore file
				File dir = app().runtime().getFileOrFolder(Keys.git.gitignoreFolder, "${baseFolder}/gitignore");
				File file = new File(dir, gitignore + ".gitignore");
				if (file.exists() && file.length() > 0) {
					byte [] bytes = FileUtils.readContent(file);
					if (!ArrayUtils.isEmpty(bytes)) {
						DirCacheEntry entry = new DirCacheEntry(".gitignore");
						entry.setLength(bytes.length);
						entry.setLastModified(System.currentTimeMillis());
						entry.setFileMode(FileMode.REGULAR_FILE);
						entry.setObjectId(odi.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes));

						indexBuilder.add(entry);
					}
				}
			}

			if (addGitFlow) {
				// insert a .gitflow file
				Config config = new Config();
				config.setString("gitflow", null, "masterBranch", Constants.MASTER);
				config.setString("gitflow", null, "developBranch", Constants.DEVELOP);
				config.setString("gitflow", null, "featureBranchPrefix", "feature/");
				config.setString("gitflow", null, "releaseBranchPrefix", "release/");
				config.setString("gitflow", null, "hotfixBranchPrefix", "hotfix/");
				config.setString("gitflow", null, "supportBranchPrefix", "support/");
				config.setString("gitflow", null, "versionTagPrefix", "");

				byte [] bytes = config.toText().getBytes(Constants.ENCODING);

				DirCacheEntry entry = new DirCacheEntry(".gitflow");
				entry.setLength(bytes.length);
				entry.setLastModified(System.currentTimeMillis());
				entry.setFileMode(FileMode.REGULAR_FILE);
				entry.setObjectId(odi.insert(org.eclipse.jgit.lib.Constants.OBJ_BLOB, bytes));

				indexBuilder.add(entry);
			}

			indexBuilder.finish();

			if (newIndex.getEntryCount() == 0) {
				// nothing to commit
				return false;
			}

			ObjectId treeId = newIndex.writeTree(odi);

			// Create a commit object
			CommitBuilder commit = new CommitBuilder();
			commit.setAuthor(author);
			commit.setCommitter(author);
			commit.setEncoding(Constants.ENCODING);
			commit.setMessage("Initial commit");
			commit.setTreeId(treeId);

			// Insert the commit into the repository
			ObjectId commitId = odi.insert(commit);
			odi.flush();

			// set the branch refs
			RevWalk revWalk = new RevWalk(db);
			try {
				// set the master branch
				RevCommit revCommit = revWalk.parseCommit(commitId);
				RefUpdate masterRef = db.updateRef(Constants.R_MASTER);
				masterRef.setNewObjectId(commitId);
				masterRef.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
				Result masterRC = masterRef.update();
				switch (masterRC) {
				case NEW:
					success = true;
					break;
				default:
					success = false;
				}

				if (addGitFlow) {
					// set the develop branch for git-flow
					RefUpdate developRef = db.updateRef(Constants.R_DEVELOP);
					developRef.setNewObjectId(commitId);
					developRef.setRefLogMessage("commit: " + revCommit.getShortMessage(), false);
					Result developRC = developRef.update();
					switch (developRC) {
					case NEW:
						success = true;
						break;
					default:
						success = false;
					}
				}
			} finally {
				revWalk.close();
			}
		} catch (UnsupportedEncodingException e) {
			logger().error(null, e);
		} catch (IOException e) {
			logger().error(null, e);
		} finally {
			odi.close();
			db.close();
		}
		return success;
	}
}
