FattyAcid.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 com.fasterxml.jackson.annotation.JsonIgnore;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Set;

/**
 * A fatty acid with a specific type. This object defines the name, position,
 * number of carbon atoms, hydroxyls and double bonds, as well as the bond type
 * to the head group. A FattyAcid can carry optional modifications / functional groups and can
 * report double bond positions.
 *
 * @author Dominik Kopczynski
 * @author Nils Hoffmann
 */
public class FattyAcid extends FunctionalGroup {
    
    public static final Set<String> fgExceptions = new HashSet<>(Arrays.asList("acyl", "alkyl", "cy", "cc", "acetoxy"));
    public static final Set<LipidFaBondType> LCB_STATES = new HashSet<>(Arrays.asList(LipidFaBondType.LCB_REGULAR, LipidFaBondType.LCB_EXCEPTION));
    protected int numCarbon;
    protected LipidFaBondType lipidFaBondType;
    
    public FattyAcid(String _name, KnownFunctionalGroups knownFunctionalGroups) {
        this(_name, 0, null, null, LipidFaBondType.ESTER, 0, knownFunctionalGroups);
    }

    public FattyAcid(String _name, int _num_carbon, DoubleBonds _double_bonds) {
        this(_name, _num_carbon, _double_bonds, null, LipidFaBondType.ESTER, 0, null);
    }

    public FattyAcid(String _name, int _num_carbon, DoubleBonds _double_bonds, KnownFunctionalGroups knownFunctionalGroups) {
        this(_name, _num_carbon, _double_bonds, null, LipidFaBondType.ESTER, 0, knownFunctionalGroups);
    }

    public FattyAcid(String _name, int _num_carbon, KnownFunctionalGroups knownFunctionalGroups) {
        this(_name, _num_carbon, null, null, LipidFaBondType.ESTER, 0, knownFunctionalGroups);
    }

    public FattyAcid(String _name, int _num_carbon, DoubleBonds _double_bonds, HashMap<String, ArrayList<FunctionalGroup>> _functional_groups, KnownFunctionalGroups knownFunctionalGroups) {
        this(_name, _num_carbon, _double_bonds, _functional_groups, LipidFaBondType.ESTER, 0, knownFunctionalGroups);
    }

    public FattyAcid(String _name, int _num_carbon, DoubleBonds _double_bonds, HashMap<String, ArrayList<FunctionalGroup>> _functional_groups, LipidFaBondType _lipid_FA_bond_type, KnownFunctionalGroups knownFunctionalGroups) {
        this(_name, _num_carbon, _double_bonds, _functional_groups, _lipid_FA_bond_type, 0, knownFunctionalGroups);
    }

    public FattyAcid(String _name, int _num_carbon, DoubleBonds _double_bonds, HashMap<String, ArrayList<FunctionalGroup>> _functional_groups, LipidFaBondType _lipid_FA_bond_type, int _position, KnownFunctionalGroups knownFunctionalGroups) {
        super(_name, _position, 1, _double_bonds, false, "", false, null, _functional_groups, knownFunctionalGroups);
        numCarbon = _num_carbon;
        lipidFaBondType = _lipid_FA_bond_type;

        if (lipidFaBondType == LipidFaBondType.LCB_REGULAR) {
            functionalGroups.put("[X]", new ArrayList<>());
            functionalGroups.get("[X]").add(knownFunctionalGroups.get("X"));
        }

        if (numCarbon < 0 || numCarbon == 1) {
            throw new ConstraintViolationException("FattyAcid must have at least 2 carbons! Got " + Integer.toString(numCarbon));
        }

        if (getPosition() < 0) {
            throw new ConstraintViolationException("FattyAcid position must be greater or equal to 0! Got " + Integer.toString(getPosition()));
        }

        if (doubleBonds.getNumDoubleBonds() < 0) {
            throw new ConstraintViolationException("FattyAcid must have at least 0 double bonds! Got " + Integer.toString(doubleBonds.getNumDoubleBonds()));
        }
    }

    @Override
    public FunctionalGroup copy() {
        DoubleBonds db = doubleBonds.copy();
        HashMap<String, ArrayList<FunctionalGroup>> fg = new HashMap<>();
        for (Entry<String, ArrayList<FunctionalGroup>> kv : functionalGroups.entrySet()) {
            fg.put(kv.getKey(), new ArrayList<>());
            for (FunctionalGroup func_group : kv.getValue()) {
                fg.get(kv.getKey()).add(func_group.copy());
            }
        }

        return new FattyAcid(getName(), numCarbon, db, fg, lipidFaBondType, getPosition(), knownFunctionalGroups);
    }

    public void setType(LipidFaBondType _lipid_FA_bond_type) {
        lipidFaBondType = _lipid_FA_bond_type;
        if (lipidFaBondType == LipidFaBondType.LCB_REGULAR && !functionalGroups.containsKey("[X]")) {
            functionalGroups.put("[X]", new ArrayList<>());
            functionalGroups.get("[X]").add(knownFunctionalGroups.get("X"));
        } else if (functionalGroups.containsKey("[X]")) {
            functionalGroups.remove("[X]");
        }

        setName((lipidFaBondType != LipidFaBondType.LCB_EXCEPTION && lipidFaBondType != LipidFaBondType.LCB_REGULAR) ? "FA" : "LCB");
    }

    public String getPrefix(LipidFaBondType lipid_FA_bond_type) {
        switch (lipid_FA_bond_type) {
            case ETHER_PLASMANYL:
                return "O-";
            case ETHER_PLASMENYL:
                return "P-";
            default:
                return "";
        }
    }

    @Override
    public int getNDoubleBonds() {
        return super.getNDoubleBonds() + ((lipidFaBondType == LipidFaBondType.ETHER_PLASMENYL) ? 1 : 0);
    }

    public boolean lipidFaBondTypePrefix(LipidFaBondType lipid_FA_bond_type) {
        return (lipid_FA_bond_type == LipidFaBondType.ETHER_PLASMANYL) || (lipid_FA_bond_type == LipidFaBondType.ETHER_PLASMENYL) || (lipid_FA_bond_type == LipidFaBondType.ETHER_UNSPECIFIED);
    }
    

    @Override
    public String toString(LipidLevel level) {
        StringBuilder fa_string = new StringBuilder();
        fa_string.append(getPrefix(lipidFaBondType));
        int num_carbons = numCarbon;
        int num_double_bonds = doubleBonds.getNumDoubleBonds();

        if (num_carbons == 0 && num_double_bonds == 0 && !LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level | LipidLevel.STRUCTURE_DEFINED.level | LipidLevel.SN_POSITION.level)) {
            return "";
        }

        if (LipidLevel.isLevel(level, LipidLevel.SN_POSITION.level | LipidLevel.MOLECULAR_SPECIES.level)) {
            ElementTable e = computeAndCopyElements();
            num_carbons = e.get(Element.C);
            num_double_bonds = getNDoubleBonds() - ((lipidFaBondType == LipidFaBondType.ETHER_PLASMENYL) ? 1 : 0);
        }

        fa_string.append(num_carbons).append(":").append(num_double_bonds);

        if (!LipidLevel.isLevel(level, LipidLevel.SN_POSITION.level | LipidLevel.MOLECULAR_SPECIES.level) && doubleBonds.doubleBondPositions.size() > 0) {
            fa_string.append("(");

            int i = 0;
            ArrayList<Integer> sorted_db = new ArrayList<>(doubleBonds.doubleBondPositions.keySet());
            Collections.sort(sorted_db, (a, b) -> (int) a - (int) b);
            for (int db_pos : sorted_db) {
                if (i++ > 0) {
                    fa_string.append(",");
                }
                fa_string.append(db_pos);
                if (LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level)) {
                    fa_string.append(doubleBonds.doubleBondPositions.get(db_pos));
                }
            }
            fa_string.append(")");
        }
        
        
        if (level == LipidLevel.COMPLETE_STRUCTURE && stereochemistry.length() > 0){
            fa_string.append("[").append(stereochemistry).append("]");
        }
        

        if (LipidLevel.isLevel(level, LipidLevel.COMPLETE_STRUCTURE.level | LipidLevel.FULL_STRUCTURE.level)) {
            ArrayList<String> fg_names = new ArrayList<>();
            for (Entry<String, ArrayList<FunctionalGroup>> kv : functionalGroups.entrySet()) {
                fg_names.add(kv.getKey());
            }
            Collections.sort(fg_names, (String a, String b) -> a.toLowerCase().compareTo(b.toLowerCase()));

            for (String fg : fg_names) {
                if (fg.equals("[X]")) {
                    continue;
                }
                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;
                fa_string.append(";");
                for (FunctionalGroup func_group : fg_list) {
                    if (i++ > 0) {
                        fa_string.append(",");
                    }
                    fa_string.append(func_group.toString(level));
                }
            }
        } else if (level == LipidLevel.STRUCTURE_DEFINED) {
            ArrayList<String> fg_names = new ArrayList<>();
            functionalGroups.entrySet().forEach(kv -> {
                fg_names.add(kv.getKey());
            });
            Collections.sort(fg_names, (String a, String b) -> a.toLowerCase().compareTo(b.toLowerCase()));

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

                if (fgExceptions.contains(fg)) {
                    fa_string.append(";");
                    int i = 0;
                    for (FunctionalGroup func_group : fg_list) {
                        if (i++ > 0) {
                            fa_string.append(",");
                        }
                        fa_string.append(func_group.toString(level));
                    }
                } else {
                    int fg_count = 0;
                    for (FunctionalGroup func_group : fg_list) {
                        fg_count += func_group.getCount();
                    }

                    if (fg_count > 1) {
                        fa_string.append(";").append(!fg_list.get(0).atomic ? ("(" + fg + ")" + Integer.toString(fg_count)) : (fg + Integer.toString(fg_count)));
                    } else {
                        fa_string.append(";").append(fg);
                    }
                }
            }

        } else {
            ElementTable func_elements = getFunctionalGroupElements();
            for (int i = 2; i < Elements.ELEMENT_ORDER.size(); ++i) {
                Element e = Elements.ELEMENT_ORDER.get(i);
                if (func_elements.get(e) > 0) {
                    fa_string.append(";").append(Elements.ELEMENT_SHORTCUT.get(e));
                    if (func_elements.get(e) > 1) {
                        fa_string.append(func_elements.get(e));
                    }
                }
            }
        }

        return fa_string.toString();
    }

    @JsonIgnore
    @Override
    public ElementTable getFunctionalGroupElements() {
        ElementTable fgElements = super.getFunctionalGroupElements();

        // subtract the invisible [X] functional group for regular LCBs
        if (lipidFaBondType == LipidFaBondType.LCB_REGULAR && functionalGroups.containsKey("O")) {
            fgElements.put(Element.O, fgElements.get(Element.O) - 1);
        }

        return fgElements;
    }

    @Override
    public void computeElements() {
        elements = new ElementTable();

        int num_double_bonds = doubleBonds.getNumDoubleBonds();
        if (lipidFaBondType == LipidFaBondType.ETHER_PLASMENYL) {
            num_double_bonds += 1;
        }

        if (numCarbon == 0 && num_double_bonds == 0) {
            elements.put(Element.H, 1);
            return;
        }

        if (lipidFaBondType != LipidFaBondType.LCB_EXCEPTION && lipidFaBondType != LipidFaBondType.LCB_REGULAR) {
            elements.put(Element.C, numCarbon); // carbon
            switch (lipidFaBondType) {
                case ESTER:
                    elements.put(Element.H, (2 * numCarbon - 1 - 2 * num_double_bonds)); // hydrogen
                    elements.put(Element.O, 1); // oxygen
                    break;
                case ETHER_PLASMENYL:
                    elements.put(Element.H, (2 * numCarbon - 1 - 2 * num_double_bonds + 2)); // hydrogen
                    break;
                case ETHER, ETHER_PLASMANYL:
                    elements.put(Element.H, ((numCarbon + 1) * 2 - 1 - 2 * num_double_bonds)); // hydrogen
                    break;
                case AMIDE:
                    elements.put(Element.H, (2 * numCarbon + 1 - 2 * num_double_bonds) - 1); // hydrogen
                    break;
                default:
                    throw new LipidException("Mass cannot be computed for fatty acyl chain with this bond type");
            }
        } else {
            // long chain base
            elements.put(Element.C, numCarbon); // carbon
            elements.put(Element.H, (2 * (numCarbon - num_double_bonds) + 1)); // hydrogen
            elements.put(Element.N, 1); // nitrogen
        }
    }

    public int getNumCarbon() {
        return numCarbon;
    }

    public void setNumCarbon(int numCarbon) {
        this.numCarbon = numCarbon;
    }

    public LipidFaBondType getLipidFaBondType() {
        return lipidFaBondType;
    }

    public void setLipidFaBondType(LipidFaBondType lipidFaBondType) {
        this.lipidFaBondType = lipidFaBondType;
    }
    
}