Cycle.java

/*
 * Copyright 2021 Dominik Kopczynski, Nils Hoffmann.
 *
 * 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 org.lifstools.jgoslin.domain;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map.Entry;

/**
 * A cyclic hydrocarbon chain as a functional group.
 *
 * @author Dominik Kopczynski
 * @author Nils Hoffmann
 */
public final class Cycle extends FunctionalGroup {

    private int cycle;
    private int start;
    private int end;
    private final ArrayList<Element> bridgeChain;

    public Cycle(int _cycle, KnownFunctionalGroups knownFunctionalGroups) {
        this(_cycle, -1, -1, null, null, null, knownFunctionalGroups);
    }

    public Cycle(int _cycle, int _start, int _end, KnownFunctionalGroups knownFunctionalGroups) {
        this(_cycle, _start, _end, null, null, null, knownFunctionalGroups);
    }

    public Cycle(int _cycle, int _start, int _end, HashMap<String, ArrayList< FunctionalGroup>> _functional_groups, KnownFunctionalGroups knownFunctionalGroups) {
        this(_cycle, _start, _end, null, _functional_groups, null, knownFunctionalGroups);

    }

    public Cycle(int _cycle, int _start, int _end, DoubleBonds _double_bonds, HashMap<String, ArrayList< FunctionalGroup>> _functional_groups, KnownFunctionalGroups knownFunctionalGroups) {
        this(_cycle, _start, _end, _double_bonds, _functional_groups, null, knownFunctionalGroups);

    }

    public Cycle(int _cycle, int _start, int _end, DoubleBonds _double_bonds, HashMap<String, ArrayList< FunctionalGroup>> _functional_groups, ArrayList< Element> _bridge_chain, KnownFunctionalGroups knownFunctionalGroups) {
        super("cy", _start, 1, _double_bonds, false, "", false, null, _functional_groups, knownFunctionalGroups);
        cycle = _cycle;
        start = _start;
        end = _end;
        elements.put(Element.H, elements.get(Element.H) - 2);
        bridgeChain = (_bridge_chain == null) ? new ArrayList<>() : _bridge_chain;
    }

    @Override
    public FunctionalGroup copy() {
        DoubleBonds db = doubleBonds.copy();
        HashMap<String, ArrayList<FunctionalGroup>> fg = new HashMap<>();
        functionalGroups.entrySet().stream().map(kv -> {
            fg.put(kv.getKey(), new ArrayList<>());
            return kv;
        }).forEachOrdered(kv -> {
            kv.getValue().forEach(func_group -> {
                fg.get(kv.getKey()).add(func_group.copy());
            });
        });
        ArrayList<Element> bc = new ArrayList<>();
        for (Element e : bridgeChain) {
            bc.add(e);
        }

        return new Cycle(cycle, start, end, db, fg, bc, knownFunctionalGroups);
    }

    public int getCycle() {
        return cycle;
    }

    public void setCycle(int cycle) {
        this.cycle = cycle;
    }

    public int getStart() {
        return start;
    }

    public void setStart(int start) {
        this.start = start;
    }

    public int getEnd() {
        return end;
    }

    public void setEnd(int end) {
        this.end = end;
    }

    public ArrayList<Element> getBridgeChain() {
        return bridgeChain;
    }

    @Override
    public int getNDoubleBonds() throws ConstraintViolationException {
        return super.getNDoubleBonds() + 1;
    }

    @Override
    public void addPosition(int pos) {
        start += (start >= pos) ? 1 : 0;
        end += (end >= pos) ? 1 : 0;
        super.addPosition(pos);
    }

    public void rearrangeFunctionalGroups(FunctionalGroup parent, int shift) {
        // put everything back into parent
        for (Entry<Integer, String> kv : doubleBonds.doubleBondPositions.entrySet()) {
            parent.doubleBonds.doubleBondPositions.put(kv.getKey(), kv.getValue());
        }
        doubleBonds = new DoubleBonds();

        for (Entry<String, ArrayList<FunctionalGroup>> kv : functionalGroups.entrySet()) {
            if (!parent.functionalGroups.containsKey(kv.getKey())) {
                parent.functionalGroups.put(kv.getKey(), new ArrayList<>());
            }
            parent.functionalGroups.get(kv.getKey()).addAll(functionalGroups.get(kv.getKey()));
        }
        functionalGroups = new HashMap<>();

        // shift the cycle
        shiftPositions(shift);

        // take back what's mine# check double bonds
        parent.doubleBonds.doubleBondPositions.entrySet().stream().filter(kv -> (start <= kv.getKey() && kv.getKey() <= end)).forEachOrdered(kv -> {
            doubleBonds.doubleBondPositions.put(kv.getKey(), kv.getValue());
        });
        doubleBonds.setNumDoubleBonds(doubleBonds.doubleBondPositions.size());

        doubleBonds.doubleBondPositions.entrySet().forEach(kv -> {
            parent.doubleBonds.doubleBondPositions.remove(kv.getKey());
        });
        parent.doubleBonds.setNumDoubleBonds(parent.doubleBonds.doubleBondPositions.size());

        HashSet<String> remove_list = new HashSet<>();
        for (Entry<String, ArrayList<FunctionalGroup>> kv : parent.functionalGroups.entrySet()) {
            ArrayList<Integer> remove_item = new ArrayList<>();

            int i = 0;
            for (FunctionalGroup func_group : kv.getValue()) {
                if (start <= func_group.getPosition() && func_group.getPosition() <= end && func_group != this) {
                    if (!functionalGroups.containsKey(kv.getKey())) {
                        functionalGroups.put(kv.getKey(), new ArrayList<>());
                    }
                    functionalGroups.get(kv.getKey()).add(func_group);
                    remove_item.add(i);
                }
                ++i;
            }

            while (remove_item.size() > 0) {
                int pos = remove_item.get(remove_item.size() - 1);
                remove_item.remove(remove_item.size() - 1);
                kv.getValue().remove(pos);
            }
            if (kv.getValue().isEmpty()) {
                remove_list.add(kv.getKey());
            }
        }

        for (String fg : remove_list) {
            parent.functionalGroups.remove(fg);
        }
    }

    @Override
    public void shiftPositions(int shift) {
        super.shiftPositions(shift);
        start += shift;
        end += shift;
        DoubleBonds db = new DoubleBonds();
        for (Entry<Integer, String> kv : doubleBonds.doubleBondPositions.entrySet()) {
            db.doubleBondPositions.put(kv.getKey() + shift, kv.getValue());
        }
        db.setNumDoubleBonds(db.doubleBondPositions.size());
        doubleBonds = db;
    }

    @Override
    public void computeElements() {
        elements = new ElementTable();
        elements.put(Element.H, -2 - 2 * doubleBonds.getNumDoubleBonds());

        for (Element chain_element : bridgeChain) {
            try {
                switch (chain_element) {
                    case C:
                        elements.put(Element.C, elements.get(Element.C) + 1);
                        elements.put(Element.H, elements.get(Element.H) + 2);
                        break;

                    case N:
                        elements.put(Element.N, elements.get(Element.N) + 1);
                        elements.put(Element.H, elements.get(Element.H) + 1);
                        break;

                    case P:
                        elements.put(Element.P, elements.get(Element.P) + 1);
                        elements.put(Element.H, elements.get(Element.H) + 1);
                        break;

                    case As:
                        elements.put(Element.As, elements.get(Element.As) + 1);
                        elements.put(Element.H, elements.get(Element.H) + 1);
                        break;

                    case O:
                        elements.put(Element.O, elements.get(Element.O) + 1);
                        break;

                    case S:
                        elements.put(Element.S, elements.get(Element.S) + 1);
                        break;

                }
            } catch (Exception e) {
                throw new ConstraintViolationException("Element '" + Elements.ELEMENT_SHORTCUT.get(chain_element) + "' cannot be part of a cycle bridge", e);
            }
        }

        // add all implicit carbon chain elements
        if (start != -1 && end != -1) {
            int n = Math.max(0, cycle - (end - start + 1 + bridgeChain.size()));
            elements.put(Element.C, elements.get(Element.C) + n);
            elements.put(Element.H, elements.get(Element.H) + 2 * n);
        }
    }

    @Override
    public String toString(LipidLevel level) {
        StringBuilder cycle_string = new StringBuilder();
        cycle_string.append("[");
        if (start != -1 && LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level)) {
            cycle_string.append(start).append("-").append(end);
        }

        if (LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level | LipidLevel.STRUCTURE_DEFINED.level) && bridgeChain.size() > 0) {
            for (Element e : bridgeChain) {
                cycle_string.append(Elements.ELEMENT_SHORTCUT.get(e));
            }
        }
        cycle_string.append("cy").append(cycle);
        cycle_string.append(":").append(doubleBonds.getNumDoubleBonds());

        if (LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level | LipidLevel.STRUCTURE_DEFINED.level)) {
            if (doubleBonds.doubleBondPositions.size() > 0) {
                int i = 0;
                cycle_string.append("(");
                ArrayList<Integer> sorted = new ArrayList<>(doubleBonds.doubleBondPositions.keySet());
                Collections.sort(sorted, (a, b) -> (int) a - (int) b);
                for (int key : sorted) {
                    String value = doubleBonds.doubleBondPositions.get(key);
                    if (i++ > 0) {
                        cycle_string.append(",");
                    }
                    if (LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level)) {
                        cycle_string.append(key).append(value);
                    } else {
                        cycle_string.append(key);
                    }
                }
                cycle_string.append(")");
            }
        }

        if (LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level)) {
            ArrayList<String> fg_names = new ArrayList<>(functionalGroups.keySet());
            Collections.sort(fg_names, (String a, String b) -> a.toLowerCase().compareTo(b.toLowerCase()));

            for (String fg : fg_names) {
                ArrayList<FunctionalGroup> fg_list = functionalGroups.get(fg);
                if (fg_list.isEmpty()) {
                    continue;
                }

                Collections.sort(fg_list, (FunctionalGroup a, FunctionalGroup b) -> a.getPosition() - b.getPosition());
                int i = 0;
                cycle_string.append(";");
                for (FunctionalGroup func_group : fg_list) {
                    if (i++ > 0) {
                        cycle_string.append(",");
                    }
                    cycle_string.append(func_group.toString(level));
                }
            }
        } else if (level == LipidLevel.STRUCTURE_DEFINED) {
            ArrayList<String> fg_names = new ArrayList<>(functionalGroups.keySet());
            Collections.sort(fg_names, (String a, String b) -> a.toLowerCase().compareTo(b.toLowerCase()));

            for (String fg : fg_names) {
                ArrayList<FunctionalGroup> fg_list = functionalGroups.get(fg);
                if (fg_list.size() > 0) {
                    if (fg_list.size() == 1 && fg_list.get(0).getCount() == 1) {
                        cycle_string.append(";").append(fg_list.get(0).toString(level));
                    } else {
                        int fg_count = 0;
                        for (FunctionalGroup func_group : fg_list) {
                            fg_count += func_group.getCount();
                        }
                        if (fg_count > 1) {
                            cycle_string.append(";(").append(fg).append(")").append(fg_count);
                        } else {
                            cycle_string.append(";").append(fg);
                        }
                    }
                }
            }
        }

        cycle_string.append("]");
        if (getStereochemistry().length() > 0) {
            cycle_string.append("[").append(getStereochemistry()).append("]");
        }

        return cycle_string.toString();
    }
}