content: /*************************************************************************
  *                                                                        *
  *  This file is part of the 20n/act project.                             *
  *  20n/act enables DNA prediction for synthetic biology/bioengineering.  *
  *  Copyright (C) 2017 20n Labs, Inc.                                     *
  *                                                                        *
  *  Please direct all queries to act@20n.com.                             *
  *                                                                        *
  *  This program is free software: you can redistribute it and/or modify  *
  *  it under the terms of the GNU General Public License as published by  *
  *  the Free Software Foundation, either version 3 of the License, or     *
  *  (at your option) any later version.                                   *
  *                                                                        *
  *  This program 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 General Public License for more details.                          *
  *                                                                        *
  *  You should have received a copy of the GNU General Public License     *
  *  along with this program.  If not, see <http://www.gnu.org/licenses/>. *
  *                                                                        *
  *************************************************************************/
  
  package com.act.biointerpretation.sars;
  
  import act.shared.Reaction;
  import chemaxon.reaction.ReactionException;
  import chemaxon.reaction.Reactor;
  import chemaxon.struc.RxnMolecule;
  import com.act.biointerpretation.mechanisminspection.ErosCorpus;
  import org.apache.logging.log4j.LogManager;
  import org.apache.logging.log4j.Logger;
  import org.json.JSONObject;
  
  import java.io.IOException;
  import java.util.ArrayList;
  import java.util.Collections;
  import java.util.HashMap;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.stream.Collectors;
  
  public class OneSubstrateOneRoCharacterizer implements ReactionGroupCharacterizer {
  
    private static final Logger LOGGER = LogManager.getFormatterLogger(OneSubstrateOneRoCharacterizer.class);
  
    private final DbAPI dbApi;
    private final FullReactionBuilder reactionBuilder;
    private final ErosCorpus roCorpus;
    private final List<SarFactory> sarFactories;
  
    public OneSubstrateOneRoCharacterizer(DbAPI dbApi,
                                          List<SarFactory> sarFactories,
                                          FullReactionBuilder reactionBuilder,
                                          ErosCorpus roCorpus) {
      this.dbApi = dbApi;
      this.sarFactories = sarFactories;
      this.reactionBuilder = reactionBuilder;
      this.roCorpus = roCorpus;
    }
  
    /**
     * Characterizes the ReactionGroup by splitting it into sets of reactions that have the same RO, and characterizing
     * each by its own generalized Reactor and SAR.
     *
     * @param group The  ReactionGroup to characterize.
     * @return A list of the resulting CharacterizedGroups.
     * @throws IOException
     */
    @Override
    public List<CharacterizedGroup> characterizeGroup(ReactionGroup group) {
      List<CharacterizedGroup> resultGroups = new ArrayList<>();
      List<Reaction> workingReactionList = dbApi.getReactions(group);
      workingReactionList.removeIf(r -> r.getSubstrates().length != 1 || r.getProducts().length != 1);
      Integer initialReactionCount = workingReactionList.size();
  
      while (workingReactionList.size() > 1) {
        Integer roId = getMostCommonRo(workingReactionList);
        // If no RO explains any of the reactions, reject this set.
        if (roId == null) {
          break;
        }
  
        List<Reaction> matchingReactions = getReactionsMatching(workingReactionList, roId);
        LOGGER.info("%d of %d reactions matching RO %d", matchingReactions.size(), initialReactionCount, roId);
        workingReactionList.removeAll(matchingReactions);
  
        if (matchingReactions.size() == 1) {
          LOGGER.warn("Group %s has only 1 substrate for RO %d", group.getName(), roId);
          continue;
        }
  
        List<RxnMolecule> rxnMolecules = dbApi.getRxnMolecules(matchingReactions);
        Optional<CharacterizedGroup> characterization = characterizeUniformGroup(rxnMolecules, roId, group.getName());
        if (characterization.isPresent()) {
          resultGroups.add(characterization.get());
        }
      }
      return resultGroups;
    }
  
    /**
     * Characterizes a group of reactions that all share the same RO by applying the SAR builders and the
     * FullReactionBuilder to the reactions. If any step fails, an empty Optional is returned.
     *
     * @param reactions The reactions the characterize.
     * @param roId The RO that matches all of the reactions.
     * @param groupName The name of the ReactionGroup.
     * @return
     */
    private Optional<CharacterizedGroup> characterizeUniformGroup(List<RxnMolecule> reactions,
                                                                  Integer roId,
                                                                  String groupName) {
      List<Sar> sars = buildSars(reactions);
  
      Reactor fullReactor;
      try {
        fullReactor = buildFullReactor(reactions, roId);
      } catch (ReactionException e) {
        return Optional.empty();
      }
  
      return Optional.of(new CharacterizedGroup(groupName, sars, new SerializableReactor(fullReactor, roId)));
    }
  
    private Reactor buildFullReactor(List<RxnMolecule> reactions, Integer roId) throws ReactionException {
      Reactor seedReactor = roCorpus.getEro(roId).getReactor();
      return reactionBuilder.buildReaction(reactions, seedReactor);
    }
  
    private List<Sar> buildSars(List<RxnMolecule> reactions) {
      List<Sar> sars = new ArrayList<>();
      for (SarFactory builder : sarFactories) {
        sars.add(builder.buildSar(reactions));
      }
      return sars;
    }
  
    /**
     * Gets the mechanistic validator result associated with the most of the reactions.
     *
     * @param reactions The reactions associated with the group.
     * @return The most common RO.
     */
    private Integer getMostCommonRo(List<Reaction> reactions) {
      Map<Integer, List<Reaction>> roIdToReactions = getRoIdToReactionsMap(reactions);
      Map<Integer, Integer> roIdToCount = roIdToReactions.entrySet().stream()
          .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().size()));
      return Collections.max(roIdToCount.entrySet(), Map.Entry.comparingByValue()).getKey();
    }
  
    /**
     * Gets all the reactions in the list which have the given RO as a mechanistic validator result.
     *
     * @param reactions The reactions associated with the group.
     * @return The set of ROs associated with all of these reactions.
     */
    private List<Reaction> getReactionsMatching(List<Reaction> reactions, Integer roId) {
      return getRoIdToReactionsMap(reactions).get(roId);
    }
  
    /**
     * Process the mechanistic validator results of the given reactions to build a map from RO ID to all reactions with
     * that ID as a validator result.
     *
     * @param reactions The reactions to process.
     * @return The map from ro id -> associated reactions.
     */
    private Map<Integer, List<Reaction>> getRoIdToReactionsMap(List<Reaction> reactions) {
      Map<Integer, List<Reaction>> roIdToReactionsMap = new HashMap<>();
  
      for (Reaction reaction : reactions) {
        JSONObject validatorResults = reaction.getMechanisticValidatorResult();
        if (validatorResults != null) {
          for (Object roId : reaction.getMechanisticValidatorResult().keySet()) {
            Integer id = Integer.parseInt(roId.toString());
            List<Reaction> reactionList = roIdToReactionsMap.get(id);
            if (reactionList == null) {
              reactionList = new ArrayList<>();
              roIdToReactionsMap.put(id, reactionList);
            }
            reactionList.add(reaction);
          }
        }
      }
  
      return roIdToReactionsMap;
    }
  }
  