/*
 * SonarQube, open source software quality management tool.
 * Copyright (C) 2008-2014 SonarSource
 * mailto:contact AT sonarsource DOT com
 *
 * SonarQube is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * SonarQube is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 */

package org.sonar.server.startup;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import org.apache.commons.lang.builder.EqualsBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.sonar.api.ServerComponent;
import org.sonar.api.rule.RuleStatus;
import org.sonar.api.server.debt.DebtRemediationFunction;
import org.sonar.api.utils.Duration;
import org.sonar.core.persistence.DbSession;
import org.sonar.core.persistence.MyBatis;
import org.sonar.core.rule.RuleDto;
import org.sonar.core.technicaldebt.db.CharacteristicMapper;
import org.sonar.core.technicaldebt.db.RequirementMigrationDto;
import org.sonar.core.template.LoadedTemplateDto;
import org.sonar.server.db.DbClient;
import org.sonar.server.rule.RegisterRules;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;

import java.util.Collection;
import java.util.List;

/**
 * This script copy every requirements from characteristics table (every row where rule_id is not null) to the rules table.
 * <p/>
 * This script need to be executed after rules registration because default debt columns (characteristics, function, coefficient and offset) has to be populated
 * in order to be able to compare default values with overridden values.
 * <p/>
 * WARNING : When updating this class, please take time to test on ALL databases!
 *
 * @since 4.3 this component could be removed after 4 or 5 releases.
 */
public class CopyRequirementsFromCharacteristicsToRules implements ServerComponent {

  private static final Logger LOGGER = LoggerFactory.getLogger(CopyRequirementsFromCharacteristicsToRules.class);

  private static final String TEMPLATE_KEY = "CopyRequirementsFromCharacteristicsToRules";

  private final DbClient dbClient;

  /**
   * @param registerRules used only to be started after init of rules
   */
  public CopyRequirementsFromCharacteristicsToRules(DbClient dbClient, RegisterRules registerRules) {
    this.dbClient = dbClient;
  }

  public void start() {
    doExecute();
  }

  private void doExecute() {
    if (dbClient.loadedTemplateDao().countByTypeAndKey(LoadedTemplateDto.ONE_SHOT_TASK_TYPE, TEMPLATE_KEY) == 0) {
      LOGGER.info("Copying requirement from characteristics to rules");
      copyRequirementsFromCharacteristicsToRules();

      LOGGER.info("Deleting requirements from characteristics");
      removeRequirementsDataFromCharacteristics();

      dbClient.loadedTemplateDao().insert(new LoadedTemplateDto(TEMPLATE_KEY, LoadedTemplateDto.ONE_SHOT_TASK_TYPE));
    }
  }

  private void copyRequirementsFromCharacteristicsToRules() {
    DbSession dbSession = dbClient.openSession(true);

    try {
      List<RequirementMigrationDto> requirementDtos = dbSession.getMapper(CharacteristicMapper.class).selectDeprecatedRequirements();
      if (requirementDtos.isEmpty()) {
        LOGGER.info("No requirement need to be copied", requirementDtos);

      } else {
        int requirementCopied = 0;

        final Multimap<Integer, RequirementMigrationDto> requirementsByRuleId = ArrayListMultimap.create();
        for (RequirementMigrationDto requirementDto : requirementDtos) {
          requirementsByRuleId.put(requirementDto.getRuleId(), requirementDto);
        }

        List<RuleDto> rules = dbClient.ruleDao().findAll(dbSession);
        for (RuleDto rule : rules) {
          Collection<RequirementMigrationDto> requirementsForRule = requirementsByRuleId.get(rule.getId());
          if (!requirementsForRule.isEmpty()) {
            convert(rule, requirementsForRule, dbSession);
            requirementCopied++;
          }
        }
        dbSession.commit();

        LOGGER.info("{} requirements have been found, {} have been copied", requirementDtos.size(), requirementCopied);
      }
    } finally {
      MyBatis.closeQuietly(dbSession);
    }
  }

  private void convert(RuleDto rule, Collection<RequirementMigrationDto> requirementsForRule, DbSession session) {
    RequirementMigrationDto enabledRequirement = enabledRequirement(requirementsForRule);

    if (enabledRequirement == null && RuleStatus.REMOVED != rule.getStatus()) {
      // If no enabled requirement is found, it means that the requirement has been disabled for this rule
      convertDisableRequirement(rule, session);

    } else if (enabledRequirement != null) {
      // If one requirement is enable, it means either that this requirement has been set from SQALE, or that it come from a XML model definition
      convertEnabledRequirement(rule, enabledRequirement, session);

      // When default values on debt are the same that ones set by SQALE, nothing to do
    }
  }

  private static RequirementMigrationDto enabledRequirement(Collection<RequirementMigrationDto> requirementsForRule) {
    return Iterables.find(requirementsForRule, new Predicate<RequirementMigrationDto>() {
      @Override
      public boolean apply(@Nullable RequirementMigrationDto input) {
        return input != null && input.isEnabled();
      }
    }, null);
  }

  private void convertDisableRequirement(RuleDto rule, DbSession session) {
    rule.setSubCharacteristicId(RuleDto.DISABLED_CHARACTERISTIC_ID);
    rule.setRemediationFunction(null);
    rule.setRemediationCoefficient(null);
    rule.setRemediationOffset(null);
    dbClient.ruleDao().update(session, rule);
  }

  private void convertEnabledRequirement(RuleDto ruleRow, RequirementMigrationDto enabledRequirement, DbSession session) {
    ruleRow.setSubCharacteristicId(enabledRequirement.getParentId() != null ? enabledRequirement.getParentId() : null);
    ruleRow.setRemediationFunction(enabledRequirement.getFunction().toUpperCase());
    ruleRow.setRemediationCoefficient(convertDuration(enabledRequirement.getCoefficientValue(), enabledRequirement.getCoefficientUnit()));
    ruleRow.setRemediationOffset(convertDuration(enabledRequirement.getOffsetValue(), enabledRequirement.getOffsetUnit()));

    // Constant/issue with coefficient is replaced by Constant/issue with offset (with no coefficient)
    if (DebtRemediationFunction.Type.CONSTANT_ISSUE.name().equals(ruleRow.getRemediationFunction())
      && ruleRow.getRemediationCoefficient() != null) {
      ruleRow.setRemediationOffset(ruleRow.getRemediationCoefficient());
      ruleRow.setRemediationCoefficient(null);
    }

    // If the coefficient of a linear or linear with offset function is null, it should be replaced by 0
    if ((DebtRemediationFunction.Type.LINEAR.name().equals(ruleRow.getRemediationFunction()) ||
      DebtRemediationFunction.Type.LINEAR_OFFSET.name().equals(ruleRow.getRemediationFunction()))
      && ruleRow.getRemediationCoefficient() == null) {
      ruleRow.setRemediationCoefficient("0" + convertUnit(enabledRequirement.getCoefficientUnit()));
      // If the offset of a constant per issue or linear with offset function is null, it should be replaced by 0
    } else if ((DebtRemediationFunction.Type.CONSTANT_ISSUE.name().equals(ruleRow.getRemediationFunction())
      || DebtRemediationFunction.Type.LINEAR_OFFSET.name().equals(ruleRow.getRemediationFunction()))
      && ruleRow.getRemediationOffset() == null) {
      ruleRow.setRemediationOffset("0" + convertUnit(enabledRequirement.getOffsetUnit()));
    }

    if (!isDebtDefaultValuesSameAsOverriddenValues(ruleRow)) {
      // Default values on debt are not the same that ones set by SQALE, update the rule
      dbClient.ruleDao().update(session, ruleRow);
    }
  }

  @CheckForNull
  @VisibleForTesting
  static String convertDuration(@Nullable Double oldValue, @Nullable String oldUnit) {
    if (oldValue != null && oldValue > 0) {
      // As value is stored in double, we have to round it in order to have an integer (for instance, if it was 1.6, we'll use 2)
      return Integer.toString((int) Math.round(oldValue)) + convertUnit(oldUnit);
    }
    return null;
  }

  @VisibleForTesting
  private static String convertUnit(@Nullable String oldUnit) {
    String unit = oldUnit != null ? oldUnit : Duration.DAY;
    return "mn".equals(unit) ? Duration.MINUTE : unit;
  }

  @VisibleForTesting
  static boolean isDebtDefaultValuesSameAsOverriddenValues(RuleDto rule) {
    return new EqualsBuilder()
      .append(rule.getDefaultSubCharacteristicId(), rule.getSubCharacteristicId())
      .append(rule.getDefaultRemediationFunction(), rule.getRemediationFunction())
      .append(rule.getDefaultRemediationCoefficient(), rule.getRemediationCoefficient())
      .append(rule.getDefaultRemediationOffset(), rule.getRemediationOffset())
      .isEquals();
  }

  private void removeRequirementsDataFromCharacteristics() {
    DbSession dbSession = dbClient.openSession(false);
    try {
      dbSession.getMapper(CharacteristicMapper.class).deleteRequirementsFromCharacteristicsTable();
      dbSession.commit();
    } finally {
      MyBatis.closeQuietly(dbSession);
    }
  }

}
