<?php
namespace local_treestudyplan;
require_once($CFG->libdir.'/externallib.php');

class studyitem {

    public const COMPETENCY = 'competency';
    public const COURSE = 'course';
    public const JUNCTION = 'junction';
    public const BADGE = 'badge';
    public const FINISH = 'finish';
    public const START = 'start';
    public const INVALID = 'invalid';


    public const TABLE = "local_treestudyplan_item";

    private static $STUDYITEM_CACHE = [];
    private $r; // Holds database record
    private $id;
    
    private $courseinfo = null;
    private $studyline;
    private $aggregator;

    public function context(): \context {
        return $this->studyline->context();
    }

    public function studyline(): studyline {
        return $this->studyline;
    }

    public function conditions() {
        return $this->r->conditions;
    }

    public static function findById($id): self {
        if(!array_key_exists($id,self::$STUDYITEM_CACHE)){
            self::$STUDYITEM_CACHE[$id] = new self($id);
        } 
        return self::$STUDYITEM_CACHE[$id];
    }


    public function __construct($id) {
        global $DB;
        $this->id = $id;
        $this->r = $DB->get_record(self::TABLE,['id' => $id],"*",MUST_EXIST);

        $this->studyline = studyline::findById($this->r->line_id);
        $this->aggregator = $this->studyline()->studyplan()->aggregator();
    }

    public function id(){
        return $this->id;
    }
    
    public function slot(){
        return $this->r->slot;
    }

    public function layer(){
        return $this->r->layer;
    }

    public function type(){
        return $this->r->type;
    }

    public function courseid(){
        return $this->r->course_id;
    }

    public static function exists($id){
        global $DB;
        return is_numeric($id) && $DB->record_exists(self::TABLE, array('id' => $id));
    }

    public static function editor_structure($value=VALUE_REQUIRED){
        return new \external_single_structure([
            "id" => new \external_value(PARAM_INT, 'id of study item'),
            "type" => new \external_value(PARAM_TEXT, 'shortname of study item'),
            "conditions"=> new \external_value(PARAM_TEXT, 'conditions for completion'),
            "slot" => new \external_value(PARAM_INT, 'slot in the study plan'),
            "layer" => new \external_value(PARAM_INT, 'layer in the slot'),
            "span"         => new \external_value(PARAM_INT, 'how many periods the item spans'),
            "course" => courseinfo::editor_structure(VALUE_OPTIONAL),
            "badge" => badgeinfo::editor_structure(VALUE_OPTIONAL),
            "continuation_id" => new \external_value(PARAM_INT, 'id of continued item'),
            "connections" => new \external_single_structure([
                'in' => new \external_multiple_structure(studyitemconnection::structure()),
                'out' => new \external_multiple_structure(studyitemconnection::structure()),
            ]),
        ]);

    }

    public function editor_model(){
        return $this->generate_model("editor");
    }

    private function generate_model($mode){
        // Mode parameter is used to geep this function for both editor model and export model
        // (Export model results in fewer parameters on children, but is otherwise basically the same as this function)
        global $DB;

        $model = [
            'id' => $this->r->id, // Id is needed in export model because of link references
            'type' => $this->isValid()?$this->r->type:self::INVALID,
            'conditions' => $this->r->conditions,
            'slot' => $this->r->slot,
            'layer' => $this->r->layer,
            'span' => $this->r->span,
            'continuation_id' => $this->r->continuation_id,
            'connections' => [
                "in" => [],
                "out" => [],
            ]
        ];
        if($mode == "export"){
            // remove slot and layer
            unset($model["slot"]);
            unset($model["layer"]);
            unset($model["continuation_id"]);
            $model["connections"] = []; // In export mode, connections is just an array of outgoing connections
            if(!isset($this->r->conditions)){
                unset($model["conditions"]);
            }
        }

        // Add course link if available
        $ci = $this->getcourseinfo();
        if(isset($ci)){
            if($mode == "export"){
                $model['course'] = $ci->shortname();
            } else {
                $model['course'] = $ci->editor_model($this,$this->aggregator->usecorecompletioninfo());
            }
        }

        // Add badge info if available
        if(is_numeric($this->r->badge_id) && $DB->record_exists('badge', array('id' => $this->r->badge_id)))
        {
            $badge = new \core_badges\badge($this->r->badge_id);
            $badgeinfo = new badgeinfo($badge);
            if($mode == "export"){
                $model['badge'] = $badgeinfo->name();
            } else {
                // Also supply a list of linked users, so the badgeinfo can give stats on 
                // the amount issued, related to this studyplan
                $studentids = $this->studyline()->studyplan()->find_linked_userids();
                $model['badge'] = $badgeinfo->editor_model($studentids);
            }
        }

        if($mode == "export"){
            // Also export gradables
            $gradables = gradeinfo::list_studyitem_gradables($this);
            if(count($gradables) > 0){
                $model["gradables"] = [];  
                foreach($gradables as $g){
                    $model["gradables"][] = $g->export_model();;
                }
            }
        }


        // Add incoming and outgoing connection info
        $conn_out = studyitemconnection::find_outgoing($this->id);

        if($mode == "export"){
            foreach($conn_out as $c) {
                $model["connections"][] = $c->to_id();
            }
        }
        else {
            foreach($conn_out as $c) {
                $model['connections']['out'][$c->to_id()] = $c->model();
            }
            $conn_in = studyitemconnection::find_incoming($this->id);
            foreach($conn_in as $c) {
                $model['connections']['in'][$c->from_id()] = $c->model();
            }
        }

        return $model;

    }

    public static function add($fields,$import=false)
    {
        global $DB;
        $addable = ['line_id','type','layer','conditions','slot','competency_id','course_id','badge_id','continuation_id','span'];
        $info = [ 'layer' => 0, ];
        foreach($addable as $f){
            if(array_key_exists($f,$fields)){
                $info[$f] = $fields[$f];
            }
        }
        $id = $DB->insert_record(self::TABLE, $info);
        $item = self::findById($id);
        if($item->type() == self::COURSE){
            // Signal the studyplan that a course has been added so it can be marked for csync cascading
            $item->studyline()->studyplan()->mark_csync_changed(); 
        }
        return $item;
    }

    public function edit($fields)
    {
        global $DB;
        $editable = ['conditions','course_id','continuation_id','span'];

        $info = ['id' => $this->id,];
        foreach($editable as $f){
            if(array_key_exists($f,$fields) && isset($fields[$f])){
                $info[$f] = $fields[$f];
            }
        }

        $DB->update_record(self::TABLE, $info);
        //reload record after edit
        $this->r = $DB->get_record(self::TABLE,['id' => $this->id],"*",MUST_EXIST);
        return $this;
    }

    public function isValid(){
        // Check if referenced courses, badges and/or competencies still exist
        if($this->r->type == static::COURSE){
            return courseinfo::exists($this->r->course_id);
        } 
        else if($this->r->type == static::BADGE){
            return badgeinfo::exists($this->r->badge_id);
        } 
        else {
            return true;
        }
    }

    public function delete($force=false)
    {
        global $DB;

        // check if this item is referenced in a START item
        if($force){
            // clear continuation id from any references to this item
            $records = $DB->get_records(self::TABLE,['continuation_id' => $this->id]);
            foreach($records as $r){
                $r->continuation_id = 0;
                $DB->update_record(self::TABLE,$r);
            }
        }

        if($DB->count_records(self::TABLE,['continuation_id' => $this->id]) > 0){
            return success::fail('Cannot remove: item is referenced by another item');
        }
        else 
        {
            // delete al related connections to this item
            studyitemconnection::clear($this->id);
            // delete all grade inclusion references to this item
            $DB->delete_records("local_treestudyplan_gradeinc",['studyitem_id' => $this->id]);
            // delete the item itself
            $DB->delete_records(self::TABLE, ['id' => $this->id]);

            return success::success();
        }
    }

    /************************
     *                      *
     * reorder_studyitems   *
     *                      *
     ************************/
    
    public static function reorder($resequence)
    {
        global $DB;

        foreach($resequence as $sq)
        {
            $DB->update_record(self::TABLE, [
                'id' => $sq['id'],
                'line_id' => $sq['line_id'],
                'slot' => $sq['slot'],
                'layer' => $sq['layer'],
            ]);
        }

        return success::success();
    }

    public static function find_studyline_children(studyline $line)
    {
        global $DB;
        $list = [];
        $ids = $DB->get_fieldset_select(self::TABLE,"id","line_id = :line_id ORDER BY layer",['line_id' => $line->id()]);
        foreach($ids as $id) {
            $item = self::findById($id,$line);
            $list[] = $item;
        }
        return $list;
    }

    private static function link_structure($value=VALUE_REQUIRED){
        return new \external_single_structure([
            "id" => new \external_value(PARAM_INT, 'id of study item'),
            "type" => new \external_value(PARAM_TEXT, 'type of study item'),
            "completion" => completion::structure(),
            "studyline" => new \external_value(PARAM_TEXT, 'reference label of studyline'), 
            "studyplan" => new \external_value(PARAM_TEXT, 'reference label of studyplan'),
        ], 'basic info of referenced studyitem', $value);
    }

    private function link_model($userid){
        global $DB;
        $line = $DB->get_record(studyline::TABLE,['id' => $this->r->line_id]);
        $plan = $DB->get_record(studyplan::TABLE,['id' => $line->studyplan_id]);

        return  [
            "id" => $this->r->id,
            "type" => $this->r->type,
            "completion" => $this->completion($userid), 
            "studyline" => $line->name(), 
            "studyplan" => $plan->name(),
        ];
    }

    public static function user_structure($value=VALUE_REQUIRED){
        return new \external_single_structure([
            "id"            => new \external_value(PARAM_INT, 'id of study item'),
            "type"          => new \external_value(PARAM_TEXT, 'type of study item'),
            "completion"    => new \external_value(PARAM_TEXT, 'completion state (incomplete|progress|completed|excellent)'),
            "slot"          => new \external_value(PARAM_INT, 'slot in the study plan'),
            "layer"         => new \external_value(PARAM_INT, 'layer in the slot'),
            "span"         => new \external_value(PARAM_INT, 'how many periods the item spans'),
            "course"        => courseinfo::user_structure(VALUE_OPTIONAL),
            "badge"         => badgeinfo::user_structure(VALUE_OPTIONAL),
            "continuation"  => self::link_structure(VALUE_OPTIONAL),
            "connections" => new \external_single_structure([
                'in' => new \external_multiple_structure(studyitemconnection::structure()),
                'out' => new \external_multiple_structure(studyitemconnection::structure()),
            ]),
        ],'Study item info',$value);

    }

    public function user_model($userid){
        global $CFG, $DB;

        $model = [
            'id' => $this->r->id,
            'type' => $this->r->type,
            'completion' => completion::label($this->completion($userid)),
            'slot' => $this->r->slot,
            'layer' => $this->r->layer,
            'span' => $this->r->span,
            'connections' => [
                "in" => [],
                "out" => [],
            ]
        ];

        // Add badge info if available
        if(badgeinfo::exists($this->r->badge_id))
        {
            $badge = new \core_badges\badge($this->r->badge_id);
            $badgeinfo = new badgeinfo($badge);
            $model['badge'] = $badgeinfo->user_model($userid);
        }

        // Add continuation_info if available
        if(self::exists($this->r->continuation_id))
        {
            $c_item = self::findById($this->r->continuation_id);
            $model['continuation'] = $c_item->link_model($userid); 
        }

        // Add course if available
        if(courseinfo::exists($this->r->course_id))
        {
            $cinfo = $this->getcourseinfo();
            $model['course'] = $cinfo->user_model($userid,$this->aggregator->usecorecompletioninfo());
        }

        // Add incoming and outgoing connection info
        $conn_out = studyitemconnection::find_outgoing($this->id);
        foreach($conn_out as $c) {
            $model['connections']['out'][$c->to_id()] = $c->model();
        }
        $conn_in = studyitemconnection::find_incoming($this->id);
        foreach($conn_in as $c) {
            $model['connections']['in'][$c->from_id()] = $c->model();
        }

        return $model;

    }

    public function getcourseinfo()
    {
        if(empty($this->courseinfo) && courseinfo::exists($this->r->course_id)){
            $this->courseinfo = new courseinfo($this->r->course_id, $this);
        }
        return $this->courseinfo;
    }

    private function completion($userid) {
        global $DB;

        if($this->isValid()){
            if(strtolower($this->r->type) == 'course'){
                // determine competency by competency completion
                $courseinfo = $this->getcourseinfo();
                return $this->aggregator->aggregate_course($courseinfo,$this,$userid);
            }
            elseif(strtolower($this->r->type) =='start'){
                // Does not need to use aggregator.
                // Either true, or the completion of the reference
                if(self::exists($this->r->continuation_id)){
                    $c_item = self::findById($this->r->continuation_id);
                    return $c_item->completion($userid);
                } else {
                    return completion::COMPLETED;
                }
            }        
            elseif(in_array(strtolower($this->r->type),['junction','finish'])){
                // completion of the linked items, according to the rule
                $in_completed = [];
                // Retrieve incoming connections
                $incoming = $DB->get_records(studyitemconnection::TABLE,['to_id' => $this->r->id]);
                foreach($incoming as $conn){
                    $item = self::findById($conn->from_id);
                    $in_completed[] = $item->completion($userid);
                }
                return $this->aggregator->aggregate_junction($in_completed,$this,$userid);
            }
            elseif(strtolower($this->r->type) =='badge'){
                global $DB;
                // badge awarded
                if(badgeinfo::exists($this->r->badge_id))
                {
                    $badge = new \core_badges\badge($this->r->badge_id);
                    if($badge->is_issued($userid)){
                        if($badge->can_expire()){
                            // get the issued badges and check if any of them have not expired yet
                            $badges_issued = $DB->get_records("badge_issued",["badge_id" => $this->r->badge_id, "user_id" => $userid]);
                            $notexpired = false;
                            $now = time();
                            foreach($badges_issued as $bi){
                                if($bi->dateexpire == null || $bi->dateexpire > $now){
                                    $notexpired = true;
                                    break;
                                }
                            }
                            return ($notexpired)?completion::COMPLETED:completion::INCOMPLETE;
                        }
                        else{
                            return completion::COMPLETED;
                        }
                    } else {
                        return completion::INCOMPLETE;
                    }
                } else {
                    return completion::INCOMPLETE;
                }
            }
            else {
                // return incomplete for other types
                return completion::INCOMPLETE;
            }
        }
        else {
            // return incomplete for other types
            return completion::INCOMPLETE;
        } 
    }

    public function duplicate($new_line){
        global $DB;
        // clone the database fields
        $fields = clone $this->r;
        // set new line id
        unset($fields->id);
        $fields->line_id = $new_line->id();
        //create new record with the new data
        $id = $DB->insert_record(self::TABLE, (array)$fields);
        $new = self::findById($id,$new_line);

        // copy the grading info if relevant
        $gradables = gradeinfo::list_studyitem_gradables($this);
        foreach($gradables as $g){
            gradeinfo::include_grade($g->getGradeitem()->id,$new,true);
        }
        return $new;
    }

    public function export_model(){
        return $this->generate_model("export");
    }

    public static function import_item($model){
        unset($model["course_id"]);
        unset($model["competency_id"]);
        unset($model["badge_id"]);
        unset($model["continuation_id"]);
        if(isset($model["course"])){
            $model["course_id"] = courseinfo::id_from_shortname(($model["course"]));
        }
        if(isset($model["badge"])){
            $model["badge_id"] = badgeinfo::id_from_name(($model["badge"]));
        }      

        $item = self::add($model,true);

        if(isset($model["course_id"])){
            // attempt to import the gradables
            foreach($model["gradables"] as $gradable){
                gradeinfo::import($item,$gradable);
            }
        }

        return $item;
    }

}