package OWL2generator;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.Properties;

/**
 *  The <b><code>WriteTriplets</code></b> class processes a user-defined
 *  file containing triplet definitions (object property relationships)
 *  and generates the corresponding OWL code snippets for inclusion
 *  in an OWL/XML ontology. This class is responsible for generating
 *  the core OWL statements representing relationships between instances
 *  (individuals) within the ontology. It relies on separate classes for
 *  processing annotations and class information.
 *  <p>
 *  <b>Key Functionalities:</b>
 *  <p>
 *  Reads Triplet Definition File: Parses a text file structured
 *  with sections containing property names, super-properties,
 *  annotations, and subject-object class pairs for each triplet.
 *  <p>
 *  Processes Triplet Definitions: Extracts property details
 *  (names, inverses, annotations), class names, and individual
 *  references from each section.
 *  <p>
 *  Generates OWL Object and Data Property Declarations: Writes OWL code
 *  for object and data properties, including super-properties and annotations.
 *  <p>
 *  Generates OWL Triplet Statements: Writes OWL code for individual
 *  instances and their relationships using the defined object
 *  properties.
 *  <p>
 *  Manages Individuals: Maintains a dictionary of class-individuals
 *  assigning unique serial numbers and storing annotations.
 *  <p>
 *  Handles I/O Operations: Manages file reading and writing operations.
 *  <p>
 *  <b>Additional Notes:</b>
 *  <p>
 *  The class assumes a pre-defined file format for the triplet data.
 *  <p>
 *  The class leverages a <code>SharedUtils</code> class for common
 *  OWL code generation tasks.
 *  <p>
 *  <b>Environment:</b>
 *  <ul>
 *  <li>    IDE:              Eclipse IDE for Java Developers
 *  <li>    Version:          2021-12 (4.22.0)
 *  <li>    Build id:         20211202-1639
 *  <li>    HW Model Name:    iMac, MacOS Monterey, 12.5.1
 *  <li>    Processor Name:   Quad-Core Intel Core i5
 *  <li>    Processor Speed:  3.2 GHz
 *  <li>    Memory:           32 GB 1867 MHz DDR3
 *  <li>    Disk:             APPLE SSD SM0256G
 *  <li>    Serial:           DGKRC080GG7V
 *  </ul>
 *  @version 1-001
 *  @since   2024/06/12
 *  @author  Edit Hlaszny (https://www.edithlaszny.eu/ edithlaszny@gmail.com)
 */
public class WriteTriplets 
{
    /**
     *  Counters to be logged
     */
    private       int     countOfIndividuals                      = 0 ;
    private       int     countOfTriplets                         = 0 ;
    /**
     *  Parameter names in the header of the triplet definition file
     */
    private final String  hdr_PREDICATE_NAME                      = "PREDICATE_NAME" ;
    private final String  hdr_OBJECT_PROPERTY_ANNOTATION          = "OBJECT_PROPERTY_ANNOTATION" ;
    private final String  hdr_INV_PREDICATE_NAME                  = "INV_PREDICATE_NAME" ;
    private final String  hdr_INV_OBJECT_PROPERTY_ANNOTATION      = "INV_OBJECT_PROPERTY_ANNOTATION" ;
    private final String  hdr_SUPER_OBJECT_PROPERTY               = "SUPER_OBJECT_PROPERTY" ;

    private final String  hdr_SUBJECT_DATA_PROPERTY_NAME          = "SUBJECT_DATA_PROPERTY_NAME" ;
    private final String  hdr_SUBJECT_DATA_PROPERTY_TYPE          = "SUBJECT_DATA_PROPERTY_TYPE" ;
    private final String  hdr_SUBJECT_DATA_PROPERTY_ANNOTATION    = "SUBJECT_DATA_PROPERTY_ANNOTATION" ;

    private final String  hdr_OBJECT_DATA_PROPERTY_NAME           = "OBJECT_DATA_PROPERTY_NAME" ;
    private final String  hdr_OBJECT_DATA_PROPERTY_TYPE           = "OBJECT_DATA_PROPERTY_TYPE" ;
    private final String  hdr_OBJECT_DATA_PROPERTY_ANNOTATION     = "OBJECT_DATA_PROPERTY_ANNOTATION" ;

    private final String  hdr_SUPER_DATA_PROPERTY                 = "SUPER_DATA_PROPERTY" ;

    private       ArrayList<Individual> individuals               = null ;
    private       int                   leadingZeroesInNameSuffix = 0 ;
    private       int                   numOfitemsInLineMax       = 4 ;
    private       int                   numOfitemsInLineMin       = 2 ;
    private       OWL2_Generator        OWL2G                     = new OWL2_Generator() ;
    private       String                dst                       = "" ;
    private       SharedUtils           shu                       = null ;
    private       String                src                       = "" ;
    private       AddClassAnnotation    aca                       = null ;  //$
    private       FileWriter            writer                    = null ;

    WriteTriplets(String classAnnotationDefinitions, //  class annotation definitions (for the individuals' names)
                  String src,                        //  triplet definitions
                  String dst,                        //  result OWL file name
                  String baseURI,                    //  base URI
                  String sectionStartToken,          //  START_SECTION
                  String sectionEndToken,            //  END_SECTION
                  String classNameToken,             //  CLASS_NAME
                  String classAnnotationToken,       //  CLASS_ANNOTATION
                  String comment,                    //  "!" - 1st character of line
                  int    leadingZeroesInNameSuffix   //
                 )
    {
        this.leadingZeroesInNameSuffix = leadingZeroesInNameSuffix ;
        this.src                       = src ;
        this.dst                       = dst ;
        
        this.shu = new SharedUtils(baseURI) ;

        try
        {
            File file = new File(dst) ;

            // Append mode (set to false for overwrite)
            this.writer = new FileWriter(file, true) ;

            /**
             *  get triplet configuration file
             */
            GetSectionsFromConfigFile gsfc = new GetSectionsFromConfigFile(src               ,
                                                                           sectionStartToken ,
                                                                           sectionEndToken   ,
                                                                           comment ) ;
            /**
             *  add OWL class annotation and build dictionary of individuals
             */
            this.aca = new AddClassAnnotation (classAnnotationDefinitions,
                                               dst,
                                               baseURI,
                                               classNameToken,
                                               classAnnotationToken,
                                               comment
                                              ) ;
            /**
             *  loading each individual with its initial serial number
             */
            this.individuals = aca.getIndividuals() ;

            /**
             *  processing each tripletSet from the triplet definition file
             */
            gsfc.getSections()
                .stream()
                .forEach((t) -> {  try
                                   {
                                       processTripletSet(t) ;
                                   }
                                   catch  (IOException e)
                                   {
                                       System.err.println("IO error in " + src +
                                                          " or " + dst + e.getMessage()) ;
                                   }
                                } );

            /**
             *  Closing the XML file
             */
            this.writer.write("</Ontology>\n");
        }
        catch (IOException e)
        {
            System.err.println("Error creating or writing to file: " + e.getMessage()) ;

        }
        finally
        {
            // Close the file (optional, as FileWriter closes automatically on close())
            try
            {
                this.writer.close(); // Close the writer explicitly here (optional)closeFile(file) ;
            }
            catch (IOException e)
            {
                System.err.println("Error closing file: " + e.getMessage()) ;
            }
        }
        
        /**
         *  Logging of the output volume
         */
        OWL2G.log(new StringBuilder()
                      .append("    defined  " + this.countOfIndividuals + " individual(s)\n")
                      .append("    defined  " + this.countOfTriplets    + " triplet(s)\n")
                      .toString()
                 ) ; 
        
    }   //  end of constructor()

    private String addLeadingZeros(int num, int totalLengthOfString)
    {
        String numberString = Integer.toString(num);
        int    zerosToAdd   = totalLengthOfString - numberString.length();

        String zeros = Stream.generate(() -> "0").limit(zerosToAdd).collect(Collectors.joining());

        return zeros + numberString;

    }   //  end of method addLeadingZeros()

    private void checkItemNumsInTripletDef(String line)
    {

        int numOfitemsInLine = getNumOfItemsInLine(line) ;

        if (numOfitemsInLine < this.numOfitemsInLineMin ||
            numOfitemsInLine > this.numOfitemsInLineMax )
        {
            showFatalErrMsgAndExit(new StringBuilder()
                                    .append("Fatal error: ")
                                    .append("invalid number of arguments " + " (")
                                    .append(numOfitemsInLine + ") in the line\n>" + line)
                                    .append("<\nof the triplet definition file: \n" + this.src)
                                    .toString()
                                  ) ;
        }

    }   //  end of method checkItemNumsInTripletDef()

    private void checkValidity(boolean[] entities, String fatalErrMsg, Properties hdr) 
    {
        if (entities == null || entities.length == 0) 
        {
            throw new IllegalArgumentException("Entities array cannot be null/empty");
        }
    
        boolean allTrue = true;
        boolean allFalse = true;
    
        for (boolean entity : entities) 
        {
            /**
             *  Check if all are true using bitwise AND
             */
            allTrue  &=  entity ;
            /**
             *  Check if all are false using NOT and bitwise AND
             */
            allFalse &= !entity ; 
        }

        /**
         *  Return true if all true or all false
         */
        if (! (allTrue || allFalse))
        {
            fatalErrMsg += "\nOWL2 Generator aborted - " +
                           "the generated OWL/XML file cannot be used." ;
            
            showFatalErrMsgAndExit(fatalErrMsg) ;   
        }

    }   //  end of method checkValidity()

    private void checkTripletSetConsistency(Properties hdr)
    {
        boolean hdrItemsPresence_1[] = 
        {   getHdrParam(hdr, this.hdr_PREDICATE_NAME).isEmpty()             , 
            getHdrParam(hdr, this.hdr_OBJECT_PROPERTY_ANNOTATION).isEmpty() ,                 
            getHdrParam(hdr, this.hdr_SUPER_OBJECT_PROPERTY).isEmpty() } ;

        checkValidity(hdrItemsPresence_1, 
                      new StringBuilder()
                          .append("\nPredicate name/annotation or super object property ")
                          .append("is undefined in: \n" + this.src)
                          .toString(), hdr) ;

        boolean hdrItemsPresence_2[] = 
        {   getHdrParam(hdr, this.hdr_INV_PREDICATE_NAME).isEmpty()             , 
            getHdrParam(hdr, this.hdr_INV_OBJECT_PROPERTY_ANNOTATION).isEmpty() } ;
         
        checkValidity(hdrItemsPresence_2, 
                      new StringBuilder()
                          .append("\nInverse predicate name/annotation or super ")
                          .append("object property is undefined in: \n" + this.src)
                          .toString(), hdr) ;
        
        boolean hdrItemsPresence_3[] = 
        {   getHdrParam(hdr, this.hdr_SUBJECT_DATA_PROPERTY_NAME).isEmpty()       ,
            getHdrParam(hdr, this.hdr_SUBJECT_DATA_PROPERTY_TYPE).isEmpty()       , 
            getHdrParam(hdr, this.hdr_SUBJECT_DATA_PROPERTY_ANNOTATION).isEmpty() ,
            getHdrParam(hdr, this.hdr_SUPER_DATA_PROPERTY).isEmpty() } ;

        checkValidity(hdrItemsPresence_3, 
                      new StringBuilder()
                          .append("\nInconsistent/indefined subject data properties ")
                          .append("in: \n" + this.src)
                          .toString(), hdr) ;
 
        boolean hdrItemsPresence_4[] = 
        {   getHdrParam(hdr, this.hdr_OBJECT_DATA_PROPERTY_NAME).isEmpty()       ,
            getHdrParam(hdr, this.hdr_OBJECT_DATA_PROPERTY_TYPE).isEmpty()       ,                 
            getHdrParam(hdr, this.hdr_OBJECT_DATA_PROPERTY_ANNOTATION).isEmpty()} ;  

        checkValidity(hdrItemsPresence_4, 
                      new StringBuilder()
                          .append("\nInconsistent/indefined object data properties ")
                          .append("in: \n" + this.src)
                          .toString(), hdr) ;

    }   //  end of method checkTripletSetConsistency()

    private String getClassAnnotation(String className)
    {
        int  individualIdx = 0 ;

        if ((individualIdx = getClassIdx(className)) == -1)
            return "THERE IS NO ANNOTATION TO THE INDIVIDUAL " + className ;
        else
            return this.individuals.get(individualIdx).annotation ;

    }   //  end of method getClassAnnotation()

    private int getClassIdx(String className)
    {
        Optional<Individual> foundIndividual =
                individuals.stream()
                           .filter(individual -> individual
                                                 .individualName
                                                 .equals(className))
                                                 .findFirst();
        if (foundIndividual.isPresent())
            return individuals.indexOf(foundIndividual.get());
        else
            return -1 ;

    }   //  end of method getClassIdx()

    private String getHdrParam(Properties hdr,
                               String     paramName
                              )
    {
        String paramvalue = hdr.getProperty(paramName) ;

        if (paramvalue == null || paramvalue.isEmpty())
            return "" ;
        else
            return removeLeadingAndTrailingWhitespaces(paramvalue) ;

    }   //  end of method getHdrParam()

    public String getIncrementedIndividualName(String className)
    {
        int  individualIdx = 0 ;

        if ((individualIdx = getClassIdx(className)) == -1)
            return "THERE IS NO ANNOTATION TO THE INDIVIDUAL." + className ;
        else
        {
            Individual individual = this.individuals.get(individualIdx) ;
            individual.incrementSerialNumber();
            this.individuals.set(individualIdx, individual) ;

            String individualName = individual.individualName +
                                    "_ID" + addLeadingZeros(individual.serialNumber,
                                                            this.leadingZeroesInNameSuffix) ;

            this.countOfIndividuals++ ;
            
            return  individualName.substring(0, 1).toLowerCase() + individualName.substring(1) ;

        }

    }   //  end of method getIncrementedIndividualName()

    private String getLineParameter(String line, int idx)
    {
        String[] parameters = line.split("\\s+");

        switch (getNumOfItemsInLine(line))
        {
            case 2:  if (idx <= 1)
                         return parameters[idx] ;
                     else
                         return "" ;

            case 3:
                     if (idx <= 2)
                         return parameters[idx] ;
                     else
                         return "" ;

            case 4:  if (idx <= 3)
                         return parameters[idx] ;
                     else
                         return "" ;

            default: return "" ;
        }

    }   //  end of method getLineParameter()

    private int getNumOfItemsInLine(String line)
    {
        line = removeLeadingAndTrailingWhitespaces(line) ;

        String[] words = line.split("\\s+");
        return   words.length ;

    }   //  end of method getNumOfitemsInLine()

    private Properties getTripletSetHdr(List<String> tripletSet)
    {
        Properties tripletSetHdr = new Properties();

        tripletSet.stream()
                  .filter(line -> !line.isEmpty() && line.charAt(0) != '!')
                  .takeWhile(line -> !line.startsWith("END_OF_HEADER"))
                  .dropWhile(line -> line.startsWith("START_OF_HEADER"))
                  .forEach((line) -> { try
                                       {
                                             tripletSetHdr.load(new StringReader(line));
                                       }
                                       catch (IOException e)
                                       {   System.err.println("Error at StringReader(): " +
                                                          e.getMessage()) ; }
                                     } ) ;
        return tripletSetHdr ;

    }   //  end of method getTripletSetHdr()

    private void processTriplet(Properties hdr,
                                String     line
                               )  throws IOException
    {
        int numberOfItemsInLine = getNumOfItemsInLine(line) ;

        checkItemNumsInTripletDef(line) ;

        String PREDICATE_NAME                 = getHdrParam(hdr, this.hdr_PREDICATE_NAME) ;
        String PREDICATE_ANNOTATION           = getHdrParam(hdr, this.hdr_OBJECT_PROPERTY_ANNOTATION) ;
        String INV_PREDICATE_NAME             = getHdrParam(hdr, this.hdr_INV_PREDICATE_NAME) ;
        String INV_OBJECT_PROPERTY_ANNOTATION = getHdrParam(hdr, this.hdr_INV_OBJECT_PROPERTY_ANNOTATION) ;

        String subject                        = getLineParameter(line, 0) ;
        String object                         = getLineParameter(line, 1) ;

        String subjectIndividual              = getIncrementedIndividualName(subject) ;
        String objectIndividual               = getIncrementedIndividualName(object ) ;

        String annotation = "" ;

        if (numberOfItemsInLine >= 2)
        {
            /**
             *  There are subject and object items only in the line have to be processed.
             *
             *  Minimum header items are:
             *      PREDICATE_NAME
             *      PREDICATE_ANNOTATION
             *      SUPER_OBJECT_PROPERTY
             *
             *  Maximum header items are:
             *      PREDICATE_NAME
             *      PREDICATE_ANNOTATION
             *      INV_PREDICATE_NAME
             *      INV_OBJECT_PROPERTY_ANNOTATION
             *      SUPER_OBJECT_PROPERTY
             */
            if (!INV_PREDICATE_NAME.isEmpty())
            {
                /**
                 *  there is inverse predicate too. so compose enhanced annotation
                 */
                annotation = new StringBuilder()
                      .append(getClassAnnotation(subject)      + "\n\n")
                      .append(getClassAnnotation(object)       + "\n\n")
                      .append("Predicate: "         + PREDICATE_NAME          + " - "  +
                                                      PREDICATE_ANNOTATION             +"\n\n")
                      .append("Inverse Predicate: " + INV_PREDICATE_NAME  + " - "  +
                                                      INV_OBJECT_PROPERTY_ANNOTATION     + "\n")
                      .append("Triplet-1 (S/P/O): " + subject + " - " + PREDICATE_NAME + " - "
                                                    + object  + "\n")
                      .append("Triplet-2: (S/P/O)"  + object  + " - " + INV_PREDICATE_NAME + " - "
                                                    + subject )
                      .toString() ;
            }
            else
            {
                /**
                 *  compose simple annotation
                 */
                annotation = new StringBuilder()
                      .append(getClassAnnotation(subject) + "\n\n")
                      .append(getClassAnnotation(object)  + "\n\n")
                      .append("Predicate: " + PREDICATE_ANNOTATION + "\n\n")
                      .toString() ;
            }

            /**
             *  generating the individuals for domain and range
             */
            this.writer.write(this.shu.genIndividual(subject,
                                                     subjectIndividual,
                                                     annotation)) ;
            this.writer.write(this.shu.genIndividual(object,
                                                     objectIndividual,
                                                     annotation)) ;
            /**
             *  generating the triplet
             */
            this.writer.write(this.shu.genTriplet(subjectIndividual,
                                                  PREDICATE_NAME,
                                                  objectIndividual)) ;
            this.countOfTriplets++ ;

            /**
             *  defining inverse properties if needed
             */
            if (!INV_PREDICATE_NAME.isEmpty())
            {
                this.writer.write(this.shu.genTriplet(objectIndividual,
                                                      INV_PREDICATE_NAME,
                                                      subjectIndividual)) ;
                this.countOfTriplets++ ;
            }
        }

        if (numberOfItemsInLine >= 3)
        {
            /**
             *  Items in the line as previously defined furthermore the
             *  following header items should be present:
             *      SUBJECT_DATA_PROPERTY_NAME
             *      SUBJECT_DATA_PROPERTY_TYPE
             *      SUBJECT_DATA_PROPERTY_ANNOTATION
             *      SUPER_DATA_PROPERTY
             */
            String SUBJECT_DATA_PROPERTY_NAME        = getHdrParam(hdr, this.hdr_SUBJECT_DATA_PROPERTY_NAME) ;
            String SUBJECT_DATA_PROPERTY_TYPE        = getHdrParam(hdr, this.hdr_SUBJECT_DATA_PROPERTY_TYPE) ;
            String SUBJECT_DATA_PROPERTY_ANNOTATION  = getHdrParam(hdr, this.hdr_SUBJECT_DATA_PROPERTY_ANNOTATION) ;
            String SUPER_DATA_PROPERTY               = getHdrParam(hdr, this.hdr_SUPER_DATA_PROPERTY) ;


            String subjectDatPropValue = getLineParameter(line, 2) ;

            this.writer.write(this.shu.declareDataProperty(SUBJECT_DATA_PROPERTY_NAME,
                                                           SUPER_DATA_PROPERTY,
                                                           SUBJECT_DATA_PROPERTY_ANNOTATION)) ;

            this.writer.write(this.shu.assertDataProperty(SUBJECT_DATA_PROPERTY_NAME,
                                                          subjectIndividual,
                                                          SUBJECT_DATA_PROPERTY_TYPE,
                                                          subjectDatPropValue)) ;
        }

        if (numberOfItemsInLine == 4)
        {
            /**
             *  Items in the line as previously defined furthermore the
             *  following header items should be present:
             *      OBJECT_DATA_PROPERTY_NAME
             *      OBJECT_DATA_PROPERTY_TYPE
             *      OBJECT_DATA_PROPERTY_ANNOTATION
             *      SUPER_DATA_PROPERTY
             */

            String OBJECT_DATA_PROPERTY_NAME         = getHdrParam(hdr, this.hdr_OBJECT_DATA_PROPERTY_NAME) ;
            String OBJECT_DATA_PROPERTY_TYPE         = getHdrParam(hdr, this.hdr_OBJECT_DATA_PROPERTY_TYPE) ;
            String OBJECT_DATA_PROPERTY_ANNOTATION   = getHdrParam(hdr, this.hdr_OBJECT_DATA_PROPERTY_ANNOTATION) ;
            String SUPER_DATA_PROPERTY               = getHdrParam(hdr, this.hdr_SUPER_DATA_PROPERTY) ;

            String objectDatPropValue = getLineParameter(line, 3) ;

            this.writer.write(this.shu.declareDataProperty(OBJECT_DATA_PROPERTY_NAME,
                                                           SUPER_DATA_PROPERTY,
                                                           OBJECT_DATA_PROPERTY_ANNOTATION)) ;

            this.writer.write(this.shu.assertDataProperty(OBJECT_DATA_PROPERTY_NAME,
                                                          objectIndividual,
                                                          OBJECT_DATA_PROPERTY_TYPE,
                                                          objectDatPropValue)) ;
        }

    }   //  end of method processTriplet()

    private void processTripletSet(List<String> tripletSet) throws IOException
    {
        Properties hdr = getTripletSetHdr(tripletSet) ;

        checkTripletSetConsistency(hdr) ;

        /**
         *  generating object property and its triplets
         */
        this.writer.write(this.shu.genObjPropDecl(getHdrParam(hdr, this.hdr_PREDICATE_NAME),
                                                  getHdrParam(hdr, this.hdr_SUPER_OBJECT_PROPERTY),
                                                  getHdrParam(hdr, this.hdr_OBJECT_PROPERTY_ANNOTATION))) ;
        /**
         *  defining inverse object property if it is defined
         */
        if (!getHdrParam(hdr, this.hdr_INV_PREDICATE_NAME).isEmpty())
        {
            writer.write(this.shu.genObjPropDecl(getHdrParam(hdr, this.hdr_INV_PREDICATE_NAME),
                                                 getHdrParam(hdr, this.hdr_SUPER_OBJECT_PROPERTY),
                                                 getHdrParam(hdr, this.hdr_INV_OBJECT_PROPERTY_ANNOTATION))) ;
            /**
             *  setting them as inverses
             */
            writer.write(shu.genInverseObjProps(getHdrParam(hdr, this.hdr_PREDICATE_NAME),
                                                getHdrParam(hdr, this.hdr_INV_PREDICATE_NAME))) ;
        }

        hdr.size() ;

        tripletSet.stream()
        .skip(hdr.size() + 2)  //  skipping the START and END__OF_TOKEN
        .forEach((line) -> {   try
                               {
                                   processTriplet(hdr, line) ;
                               }
                               catch  (IOException e)
                               {
                                   System.err.println("IO error in " + this.dst + e.getMessage()) ;
                               }
                           }
                ) ;

    }   //  end of method processTripletSet()

    private String removeLeadingAndTrailingWhitespaces(String s)
    {
        if (s == null || s.isEmpty())
            return "" ;

        final StringBuilder sb = new StringBuilder(s);

        /**
         *  delete from the beginning
         */
        while (sb.length() > 0 && Character.isWhitespace(sb.charAt(0)))
            sb.deleteCharAt(0) ;

        /**
         *  delete from the end
         */
        while (sb.length() > 0 && Character.isWhitespace(sb.charAt(sb.length() - 1)))
            sb.deleteCharAt(sb.length() - 1) ;

        return sb.toString() ;

    }   //  end of method removeLeadingAndTrailingWhitespaces()

    private void showFatalErrMsgAndExit(String fatalErrMsg)
    {
        System.out.println(fatalErrMsg) ;
        System.exit(-1);
    }

}   //  end of class WriteTriplets
