<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * This file contains the combined document class for the assignfeedback_editpdf plugin.
 *
 * @package   assignfeedback_editpdf
 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

namespace assignfeedback_editpdf;

defined('MOODLE_INTERNAL') || die();

/**
 * The combined_document class for the assignfeedback_editpdf plugin.
 *
 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class combined_document {

    /**
     * Status value representing a conversion waiting to start.
     */
    const STATUS_PENDING_INPUT = 0;

    /**
     * Status value representing all documents ready to be combined.
     */
    const STATUS_READY = 1;

    /**
     * Status value representing all documents are ready to be combined as are supported.
     */
    const STATUS_READY_PARTIAL = 3;

    /**
     * Status value representing a successful conversion.
     */
    const STATUS_COMPLETE = 2;

    /**
     * Status value representing a permanent error.
     */
    const STATUS_FAILED = -1;

    /**
     * The list of files which make this document.
     */
    protected $sourcefiles = [];

    /**
     * The resultant combined file.
     */
    protected $combinedfile;

    /**
     * The combination status.
     */
    protected $combinationstatus = null;

    /**
     * The number of pages in the combined PDF.
     */
    protected $pagecount = 0;

    /**
     * Check the current status of the document combination.
     * Note that the combined document may not contain all the source files if some of the
     * source files were not able to be converted. An example is an audio file with a pdf cover sheet. Only
     * the cover sheet will be included in the combined document.
     *
     * @return  int
     */
    public function get_status() {
        if ($this->combinedfile) {
            // The combined file exists. Report success.
            return self::STATUS_COMPLETE;
        }

        if (empty($this->sourcefiles)) {
            // There are no source files to combine.
            return self::STATUS_FAILED;
        }

        if (!empty($this->combinationstatus)) {
            // The combination is in progress and has set a status.
            // Return it instead.
            return $this->combinationstatus;
        }

        $pending = false;
        $partial = false;
        foreach ($this->sourcefiles as $file) {
            // The combined file has not yet been generated.
            // Check the status of each source file.
            if (is_a($file, \core_files\conversion::class)) {
                $status = $file->get('status');
                switch ($status) {
                    case \core_files\conversion::STATUS_IN_PROGRESS:
                    case \core_files\conversion::STATUS_PENDING:
                        $pending = true;
                        break;

                    // There are 4 status flags, so the only remaining one is complete which is fine.
                    case \core_files\conversion::STATUS_FAILED:
                        $partial = true;
                        break;
                }
            }
        }
        if ($pending) {
            return self::STATUS_PENDING_INPUT;
        } else {
            if ($partial) {
                return self::STATUS_READY_PARTIAL;
            }
            return self::STATUS_READY;
        }
    }
    /**
     * Set the completed combined file.
     *
     * @param   \stored_file $file The completed document for all files to be combined.
     * @return  $this
     */
    public function set_combined_file($file) {
        $this->combinedfile = $file;

        return $this;
    }

    /**
     * Return true of the combined file contained only some of the submission files.
     *
     * @return  boolean
     */
    public function is_partial_conversion() {
        $combinedfile = $this->get_combined_file();
        if (empty($combinedfile)) {
            return false;
        }
        $filearea = $combinedfile->get_filearea();
        return $filearea == document_services::PARTIAL_PDF_FILEAREA;
    }

    /**
     * Retrieve the completed combined file.
     *
     * @return  stored_file
     */
    public function get_combined_file() {
        return $this->combinedfile;
    }

    /**
     * Set all source files which are to be combined.
     *
     * @param   \stored_file|conversion[] $files The complete list of all source files to be combined.
     * @return  $this
     */
    public function set_source_files($files) {
        $this->sourcefiles = $files;

        return $this;
    }

    /**
     * Add an additional source file to the end of the existing list.
     *
     * @param   \stored_file|conversion $file The file to add to the end of the list.
     * @return  $this
     */
    public function add_source_file($file) {
        $this->sourcefiles[] = $file;

        return $this;
    }

    /**
     * Retrieve the complete list of source files.
     *
     * @return  stored_file|conversion[]
     */
    public function get_source_files() {
        return $this->sourcefiles;
    }

    /**
     * Refresh the files.
     *
     * This includes polling any pending conversions to see if they are complete.
     *
     * @return  $this
     */
    public function refresh_files() {
        $converter = new \core_files\converter();
        foreach ($this->sourcefiles as $file) {
            if (is_a($file, \core_files\conversion::class)) {
                $status = $file->get('status');
                switch ($status) {
                    case \core_files\conversion::STATUS_COMPLETE:
                        continue 2;
                        break;
                    default:
                        $converter->poll_conversion($conversion);
                }
            }
        }

        return $this;
    }

    /**
     * Combine all source files into a single PDF and store it in the
     * file_storage API using the supplied contextid and itemid.
     *
     * @param   int $contextid The contextid for the file to be stored under
     * @param   int $itemid The itemid for the file to be stored under
     * @return  $this
     */
    public function combine_files($contextid, $itemid) {
        global $CFG;

        $currentstatus = $this->get_status();
        $readystatuslist = [self::STATUS_READY, self::STATUS_READY_PARTIAL];
        if ($currentstatus === self::STATUS_FAILED) {
            $this->store_empty_document($contextid, $itemid);

            return $this;
        } else if (!in_array($currentstatus, $readystatuslist)) {
            // The document is either:
            // * already combined; or
            // * pending input being fully converted; or
            // * unable to continue due to an issue with the input documents.
            //
            // Exit early as we cannot continue.
            return $this;
        }

        require_once($CFG->libdir . '/pdflib.php');

        $pdf = new pdf();
        $files = $this->get_source_files();
        $compatiblepdfs = [];

        foreach ($files as $file) {
            // Check that each file is compatible and add it to the list.
            // Note: We drop non-compatible files.
            $compatiblepdf = false;
            if (is_a($file, \core_files\conversion::class)) {
                $status = $file->get('status');
                if ($status == \core_files\conversion::STATUS_COMPLETE) {
                    $compatiblepdf = pdf::ensure_pdf_compatible($file->get_destfile());
                }
            } else {
                $compatiblepdf = pdf::ensure_pdf_compatible($file);
            }

            if ($compatiblepdf) {
                $compatiblepdfs[] = $compatiblepdf;
            }
        }

        $tmpdir = make_request_directory();
        $tmpfile = $tmpdir . '/' . document_services::COMBINED_PDF_FILENAME;

        try {
            $pagecount = $pdf->combine_pdfs($compatiblepdfs, $tmpfile);
            $pdf->Close();
        } catch (\Exception $e) {
            // Unable to combine the PDF.
            debugging('TCPDF could not process the pdf files:' . $e->getMessage(), DEBUG_DEVELOPER);

            $pdf->Close();
            return $this->mark_combination_failed();
        }

        // Verify the PDF.
        $verifypdf = new pdf();
        $verifypagecount = $verifypdf->load_pdf($tmpfile);
        $verifypdf->Close();

        if ($verifypagecount <= 0) {
            // No pages were found in the combined PDF.
            return $this->mark_combination_failed();
        }

        // Store the newly created file as a stored_file.
        $this->store_combined_file($tmpfile, $contextid, $itemid, ($currentstatus == self::STATUS_READY_PARTIAL));

        // Note the verified page count.
        $this->pagecount = $verifypagecount;

        return $this;
    }

    /**
     * Mark the combination attempt as having encountered a permanent failure.
     *
     * @return  $this
     */
    protected function mark_combination_failed() {
        $this->combinationstatus = self::STATUS_FAILED;

        return $this;
    }

    /**
     * Store the combined file in the file_storage API.
     *
     * @param   string $tmpfile The path to the file on disk to be stored.
     * @param   int $contextid The contextid for the file to be stored under
     * @param   int $itemid The itemid for the file to be stored under
     * @param   boolean $partial The combined pdf contains only some of the source files.
     * @return  $this
     */
    protected function store_combined_file($tmpfile, $contextid, $itemid, $partial = false) {
        // Store the file.
        $record = $this->get_stored_file_record($contextid, $itemid, $partial);
        $fs = get_file_storage();

        // Delete existing files first.
        $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);

        // This was a combined pdf.
        $file = $fs->create_file_from_pathname($record, $tmpfile);

        $this->set_combined_file($file);

        return $this;
    }

    /**
     * Store the empty document file in the file_storage API.
     *
     * @param   int $contextid The contextid for the file to be stored under
     * @param   int $itemid The itemid for the file to be stored under
     * @return  $this
     */
    protected function store_empty_document($contextid, $itemid) {
        // Store the file.
        $record = $this->get_stored_file_record($contextid, $itemid);
        $fs = get_file_storage();

        // Delete existing files first.
        $fs->delete_area_files($record->contextid, $record->component, $record->filearea, $record->itemid);

        $file = $fs->create_file_from_string($record, base64_decode(document_services::BLANK_PDF_BASE64));
        $this->pagecount = 1;

        $this->set_combined_file($file);

        return $this;
    }

    /**
     * Get the total number of pages in the combined document.
     *
     * If there are no pages, or it is not yet possible to count them a
     * value of 0 is returned.
     *
     * @return  int
     */
    public function get_page_count() {
        if ($this->pagecount) {
            return $this->pagecount;
        }

        $status = $this->get_status();

        if ($status === self::STATUS_FAILED) {
            // The empty document will be returned.
            return 1;
        }

        if ($status !== self::STATUS_COMPLETE) {
            // No pages yet.
            return 0;
        }

        // Load the PDF to determine the page count.
        $temparea = make_request_directory();
        $tempsrc = $temparea . "/source.pdf";
        $this->get_combined_file()->copy_content_to($tempsrc);

        $pdf = new pdf();
        $pagecount = $pdf->load_pdf($tempsrc);
        $pdf->Close();

        if ($pagecount <= 0) {
            // Something went wrong. Return an empty page count again.
            return 0;
        }

        $this->pagecount = $pagecount;
        return $this->pagecount;
    }

    /**
     * Get the total number of documents to be combined.
     *
     * @return  int
     */
    public function get_document_count() {
        return count($this->sourcefiles);
    }

    /**
     * Helper to fetch the stored_file record.
     *
     * @param   int $contextid The contextid for the file to be stored under
     * @param   int $itemid The itemid for the file to be stored under
     * @param   boolean $partial The combined file contains only some of the source files.
     * @return  stdClass
     */
    protected function get_stored_file_record($contextid, $itemid, $partial = false) {
        $filearea = document_services::COMBINED_PDF_FILEAREA;
        if ($partial) {
            $filearea = document_services::PARTIAL_PDF_FILEAREA;
        }
        return (object) [
            'contextid' => $contextid,
            'component' => 'assignfeedback_editpdf',
            'filearea' => $filearea,
            'itemid' => $itemid,
            'filepath' => '/',
            'filename' => document_services::COMBINED_PDF_FILENAME,
        ];
    }
}
