<?php
#-------------------------------------------------------------------------
# Module: Newsletter Made Simple
#        (c) to 2008 by Paul Lempkepf
#        (c) 2008 and on by Robert Campbell (calguy1000@cmsmadesimple.org)
#
#  An addon module for CMS Made Simple to provide a flexible
#  mailing list solution.
#
#-------------------------------------------------------------------------
# CMSMS - CMS Made Simple is (c) 2005 by Ted Kulp (wishy@cmsmadesimple.org)
# Visit the CMSMS Homepage at: http://www.cmsmadesimple.org
#
#-------------------------------------------------------------------------
#
# This program 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 2 of the License, or
# (at your option) any later version.
#
# However, as a special exception to the GPL, this software is distributed
# as an addon module to CMS Made Simple.  You may not use this software
# in any Non GPL version of CMS Made simple, or in any version of CMS
# Made simple that does not indicate clearly and obviously in its admin
# section that the site was built with CMS Made simple.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
# Or read it online: http://www.gnu.org/licenses/licenses.html#GPL
#
#-------------------------------------------------------------------------
#END_LICENSE

final class nms_job implements ArrayAccess
{
    private $_data = array();
    private $_dirty = FALSE;
    private $_parts;

    /**
     * Create a new, empty job
     */
    public function __construct()
    {
        $this->_data = array('jobid'=>null,'title'=>null,'created'=>null,'started'=>null,'finished'=>null,'status'=>NMS_JOBSTATUS_UNSTARTED,'extra'=>null);
    }

    private function _fix_key($key)
    {
        $key = strtolower(trim($key));
        if( $key == 'id' ) $key = 'jobid';
        if( $key == 'name ') $key = 'title';
        return $key;
    }

    /**
     * @ignore
     */
    public function OffsetGet($key)
    {
        $key = $this->_fix_key($key);
        switch( $key ) {
	case 'name':
 	   return $this->_data['title'];
	case 'id':
	    return $this->_data['jobid'];
        case 'jobid':
        case 'title':
        case 'created':
        case 'started':
        case 'finished':
        case 'status':
            return $this->_data[$key];
        case 'async_id':
            if( isset($this->_data['extra']['async_id']) ) return $this->_data['extra']['async_id'];
            break;
        case 'parts':
            return $this->_parts;
        default:
            throw new Exception($key.' is not a valid key for a '.__CLASS__.' object');
        }
    }

    /**
     * @ignore
     */
    public function OffsetSet($key,$value)
    {
        $key = $this->_fix_key($key);
        switch( $key ) {
        case 'jobid':
            $value = (int)$value;
            if( $value < 1 ) throw new Exception('Invalid value '.$value.' for key '.$key.' in class '.__CLASS__);
            break;

        case 'title':
            $value = trim($value);
            if( !$value ) throw new Exception('Invalid value '.$value.' for key '.$key.' in class '.__CLASS__);
            break;

        case 'created':
        case 'finished':
        case 'started':
        case 'parts':
            // cannot be set directly
            throw new Exception('Cannot directly modify '.$key.' value in class '.__CLASS__);
            break;

        case 'async_id':
            if( !isset($this->_data['extra']) ) $this->_data['extra'] = [];
            $this->_data['extra'][$key] = (int) $value;
            break;

        case 'extra': // do nothing here
            break;

        case 'status':
            switch( $value ) {
            case NMS_JOBSTATUS_UNSTARTED:
            case NMS_JOBSTATUS_RUNNING:
            case NMS_JOBSTATUS_PAUSED:
            case NMS_JOBSTATUS_ERROR:
            case NMS_JOBSTATUS_ABORTED:
            case NMS_JOBSTATUS_FINISHING:
                break;

            default:
                throw new Exception('Invalid status '.$value.' for key '.$key.' in class '.__CLASS__);
                break;
            }
            break;

        default:
            throw new Exception('The key '.$key.' cannot be set/modified for a '.__CLASS__.' object');
        }

        $this->_data[$key] = $value;
        $this->_dirty = TRUE;
    }

    /**
     * @ignore
     */
    public function OffsetExists($key)
    {
        $key = $this->_fix_key($key);
        switch( $key ) {
        case 'jobid':
        case 'title':
        case 'created':
        case 'started':
        case 'finished':
        case 'status':
        case 'extra':
            return !is_null($this->_data[$key]);
        case 'async_id':
            return isset($this->_data['extra'][$key]);
        case 'parts':
            return (is_array($this->_parts) && count($this->_parts))?TRUE:FALSE;
        default:
            throw new Exception($key.' is not a valid key for a '.__CLASS__.' object');
        }
    }

    /**
     * @ignore
     */
    public function OffsetUnset($key)
    {
        $key = $this->_fix_key($key);
        switch( $key ) {
        case 'jobid':
        case 'title':
        case 'created':
        case 'started':
        case 'finished':
        case 'status':
            throw new Exception('Cannot unset this field in a '.__CLASS__);

        case 'extra':
            break;

        case 'async_id':
            if( isset($this->_data['extra'][$key]) ) unset($this->_data['extra'][$key]);
            break;

        case 'parts':
            $this->_parts = null;
            break;
        }
        $this->_dirty = TRUE;
    }

    /**
     * Add a part (list, and message) to the job
     */
    public function add_part($listid, $messageid)
    {
        $listid = (int)$listid;
        $messageid = (int)$messageid;
        if( $listid < 1 || $messageid < 1 ) throw new Exception('Invalid part specification for '.__CLASS__);

        if( is_array($this->_parts) ) {
            foreach( $this->_parts as $part ) {
                if( $part['listid'] == $listid && $part['messageid'] == $messageid ) return;
            }
        }
        else {
            $this->_parts = array();
        }

        $this->_parts[] = array('listid'=>$listid,'messageid'=>$messageid,'status'=>0);
        $this->_dirty = TRUE;
    }

    /**
     * Remove all parts from the job
     * can only be used if the job has not been started.
     */
    public function clear_parts()
    {
        $mod = cms_utils::get_module(MOD_NMS);
        if( $this['status'] != NMS_JOBSTATUS_UNSTARTED ) throw new Exception($mod->Lang('error_badjobstatus'));
        $this->_parts = null;
        $this->_dirty = true;
    }

    /**
     * Retrieve an array of lists that this job will send to
     * (parses the unique list ids out of the parts)
     *
     * @return int[]
     */
    public function get_lists()
    {
        $out = array();
        if( is_array($this->_parts) && count($this->_parts) ) {
            foreach( $this->_parts as $part ) {
                $out[] = $part['listid'];
            }
        }
        return array_unique($out);
    }

    /**
     * Return an array of messages that this job will send.
     * (parses the message ids out of the parts)
     *
     * @return int[]
     */
    public function get_messages()
    {
        $out = array();
        if( is_array($this->_parts) && count($this->_parts) ) {
            foreach( $this->_parts as $part ) {
                $out[] = $part['messageid'];
            }
        }
        return array_unique($out);
    }

    /**
     * Set the job to finished.
     * Does not save the job, or delete any async job.
     */
    public function set_finished()
    {
        $this['status'] = NMS_JOBSTATUS_FINISHED;
        $this['finished'] = time();
    }

    /**
     * Set the job to started
     * Does not save the job, or create any async job.
     */
    private function set_started()
    {
        $this['status'] = NMS_JOBSTATUS_STARTING;
        $this['started'] = time();
    }

    /**
     * Load a job, given its id
     *
     * @param int The job id
     * @return nms_job
     */
    public static function &load($id,$force = false)
    {
        $id = (int)$id;
        if( $id < 1 ) throw new Exception('Invalid job id '.$id.' for '.__CLASS__);

        $db = cmsms()->GetDb();
        $query = 'SELECT * FROM '.NMS_JOBS_TABLE.' WHERE jobid = ?';
        $row = $db->GetRow($query,array($id));
        if( $row['started'] ) $row['started'] = $db->UnixTimeStamp($row['started']);
        if( $row['finished'] ) $row['finished'] = $db->UnixTimeStamp($row['finished']);
        if( $row['created'] ) $row['created'] = $db->UnixTimeStamp($row['created']);
        if( $row['extra'] ) $row['extra'] = unserialize($row['extra']);
        if( !is_array($row) ) throw new Exception('Job '.$id.' not found');

        $query = 'SELECT listid,messageid,status FROM '.NMS_JOB_PARTS_TABLE.' WHERE jobid = ?';
        $parts = $db->GetArray($query,array($id));
        if( !$force && (!is_array($parts) || count($parts) == 0) ) throw new Exception('No parts found for job '.$id);

        $obj = new self();
        $obj->_data = $row;
        $obj->_parts = $parts;
        $obj->_dirty = FALSE;
        return $obj;
    }

    private function _save_parts()
    {
        if( $this->_data['jobid'] < 1 ) throw new Exception('Cannot save job parts -- no jobid');
        if( !is_array($this->_parts) || count($this->_parts) == 0 ) throw new Exception('Cannot save job parts -- no parts');

        $db = cmsms()->GetDb();
        $query = 'DELETE FROM '.NMS_JOB_PARTS_TABLE.' WHERE jobid = ?';
        $db->Execute($query,array($this->_data['jobid']));

        $query = 'INSERT INTO '.NMS_JOB_PARTS_TABLE.' (jobid,listid,messageid,status) VALUES (?,?,?,?)';
        foreach( $this->_parts as $part ) {
            $db->Execute($query,array($this->_data['jobid'],$part['listid'],$part['messageid'],$part['status']));
        }
    }

    private function _update()
    {
        $db = cmsms()->GetDb();
        $query = 'UPDATE '.NMS_JOBS_TABLE.' SET title = ?, started = ?, finished = ?, status = ?, extra = ? WHERE jobid = ?';
        $started = ( $this->_data['started'] ) ? trim($db->DbTimeStamp($this->_data['started']),"'") : null;
        $finished = ( $this->_data['finished'] ) ? trim($db->DbTimeStamp($this->_data['finished']),"'") : null;
        $extra = $this->_data['extra'];
        if( $extra ) $extra = serialize($extra);
        $dbr = $db->Execute($query,array($this['title'],$started,$finished,(int)$this->_data['status'],$extra,(int)$this->_data['jobid']));
        if( !$dbr ) throw new Exception($db->sql.' -- '.$db->ErrorMsg());
    }

    private function _insert()
    {
        $db = cmsms()->GetDb();
        $query = 'INSERT INTO '.NMS_JOBS_TABLE.' (title, created, started, finished, status, extra) VALUES (?,?,?,?,?,?)';
        $created = trim($db->DbTimeStamp(time()),"'");
        $extra = $this->_data['extra'];
        if( $extra ) $extra = serialize($extra);
        $dbr = $db->Execute($query,array($this->_data['title'], $created, null, null, (int)$this->_data['status'], $extra));
        if( !$dbr ) throw new Exception($db->sql.' -- '.$db->ErrorMsg());
        $this->_data['jobid'] = $db->Insert_ID();
    }

    /**
     * Save the job to the database
     */
    public function save()
    {
        if( !$this->_dirty ) return;
        if( !is_array($this->_parts) || count($this->_parts) == 0 ) throw new Exception('Cannot save a job with no parts');

        if( $this['id'] > 0 ) {
            // it's an update
            $this->_update();

            $this->_save_parts();
            $this->_dirty = FALSE;
        }
        else {
            // its an insert
            $this->_insert();

            $this->_save_parts();
            $this->_dirty = FALSE;

            // Send an event
            $mod = cms_utils::get_module(MOD_NMS);
            $parms = array();
            $parms['jobid'] = $this['id'];
            $parms['jobname'] = $this['title'];
            $parms['lists'] = $this->get_lists();
            $mod->SendEvent('OnCreateJob',$parms);
            audit((int)$this['id'],$mod->GetName(),'Job entitled: '.$this['title'].' created');
        }
    }

    /**
     * Delete this job
     * independent of status, this action will delete the job from the database
     * and all of its associated data.  As well as any async job associated with this nms job.
     *
     * The object is then reset to unstarted, and to dirty, so that saving the object again
     * will result in a new id.
     */
    public function delete()
    {
        $db = cmsms()->GetDb();
        $mod = cms_utils::get_module(MOD_NMS);

        // delete the async job if there is one.
        $async_id = $this['async_id'];
        if( $async_id > 0 ) {
            $jm = \CMSMS\Async\JobManager::get_instance();
            $job = $jm->load_job( $async_id );
            if( $job ) $jm->delete_job( $job );
        }

        $q = 'DELETE FROM '.NMS_MAILS_TABLE.' WHERE jobid = ?';
        $dbresult = $db->Execute( $q, array( (int)$this['id'] ) );

        // delete the job parts
        $q = "DELETE FROM ".NMS_JOB_PARTS_TABLE." WHERE jobid = ?";
        $dbresult = $db->Execute( $q, array( (int)$this['id'] ) );
        $this->_parts = null;

        // then the job itself
        $q = "DELETE FROM ".NMS_JOBS_TABLE." WHERE jobid = ?";
        $dbresult = $db->Execute( $q, array( (int)$this['id'] ) );

        // send an event
        $mod->SendEvent('OnDeleteJob',array('jobid'=>(int)$this['id']));

        audit((int)$this['id'],$mod->GetName(),'Deleted Job entitled: '.$this['title']);

        $this->_data = array('jobid'=>null,'title'=>null,'created'=>null,'started'=>null,'finished'=>null,'status'=>NMS_JOBSTATUS_UNSTARTED,'extra'=>null);
        $this->_dirty = TRUE;
    }

    /**
     * Start the job.
     *
     * This can only be used with saved jobs, who's status is unstarted.  The job also must have one or more parts.
     * This method automatically saves the object.
     */
    public function start()
    {
        $mod = cms_utils::get_module(MOD_NMS);
        if( $this['id'] < 1 ) throw new Exception($mod->Lang('error_jobnotsaved'));
        if( $this['status'] != NMS_JOBSTATUS_UNSTARTED ) throw new \Exception($mod->Lang('error_badjobstatus'));
        if( !is_array($this->_parts) || count($this->_parts) == 0 ) throw new \Exception($mod->Lang('error_nojobparts'));
        if( isset($this['async_id']) && $this['async_id'] > 0 ) throw new \Exception($mod->Lang('error_alreadystarted'));

        $db = cmsms()->GetDb();

        // make sure that the job has nothing in the mails table
        $query = 'SELECT jobid FROM '.NMS_MAILS_TABLE.' WHERE jobid = ?';
        $tmp = $db->GetOne($query,array($this['id']));
        if( $tmp ) throw new Exception($mod->Lang('error_dberror').' - mails exist');

        $first_task = new \NMS\InitializeJob_CmsJob( $this['id'] );
        $taskid = $first_task->save();

        if( !is_array($this->_data['extra']) ) $this->_data['extra'] = array();
        $this->_data['extra']['async_id'] = $taskid;
        $this->_data['status'] = NMS_JOBSTATUS_STARTING;
        $this->_data['started'] = time();
        $this->_dirty = TRUE;
        $this->save();
    }

    /**
     * Pause the job.
     *
     * This can only be used with saved jobs, who's status is running.
     * This method automatically saves the object.
     */
    public function pause()
    {
        $mod = cms_utils::get_module(MOD_NMS);
        if( $this['id'] < 1 ) throw new Exception($mod->Lang('error_jobnotsaved'));
        if( $this['status'] != NMS_JOBSTATUS_RUNNING ) throw new Exception($mod->Lang('error_badjobstatus'));
        if( !isset($this['async_id']) || $this['async_id'] < 1 ) throw new Exception($mod->Lang('error_internal').' 0001');
        audit('','NMS','Pause job temporarily disabled');
        return;

        /*
        $async_id = $this['async_id'];
        if( $async_id > 0 ) {
            $jm = \CMSMS\Async\JobManager::get_instance();
            $job = $jm->load_job( $async_id );
            if( is_object($job) && $job->id == $async_id ) $jm->delete_job( $job );
        }

        $this['status'] = NMS_JOBSTATUS_PAUSED;
        $this->save();
        */
    }

    /**
     * Resume a paused job.
     *
     * This can only be used with saved jobs, who's status is paused.
     * This method automatically saves the object.
     */
    public function resume()
    {
        $mod = cms_utils::get_module(MOD_NMS);
        if( $this['id'] < 1 ) throw new Exception($mod->Lang('error_jobnotsaved'));
        if( $this['status'] != NMS_JOBSTATUS_PAUSED ) throw new Exception($mod->Lang('error_badjobstatus'));
        if( !isset($this['async_id']) || $this['async_id'] < 1 ) throw new Exception($mod->Lang('error_internal').' 0001');
        audit('','NMS','resume job temporarily disabled');
        return;

        /*
        $async_id = $this['async_id'];
        if( $async_id > 0 ) {
            $jm = \CMSMS\Async\JobManager::get_instance();
            $job = $jm->load_job( $async_id );
            if( is_object($job) && $job->id == $async_id ) {
                $job->set_status(cgjobmgr_job::STATUS_INPROGRESS);
                $job->save();
            }
        }

        $this['status'] = NMS_JOBSTATUS_RUNNING;
        $this->save();
        */
    }


    /**
     * Abort a job.
     *
     * This can only be used with saved jobs, who's status is running (i.e: not on paused or error jobs)
     * This method automatically saves the object.
     */
    public function abort()
    {
        $mod = cms_utils::get_module(MOD_NMS);
        if( $this['id'] < 1 ) throw new \Exception($mod->Lang('error_jobnotsaved'));
        if( $this['status'] != NMS_JOBSTATUS_RUNNING ) throw new \Exception($mod->Lang('error_badjobstatus'));
        if( !isset($this['async_id']) || $this['async_id'] < 1 ) throw new \Exception($mod->Lang('error_internal').' 0001');

        $async_id = $this['async_id'];
        if( $async_id > 0 ) {
            $jm = \CMSMS\Async\JobManager::get_instance();
            $job = $jm->load_job( $async_id );
            if( is_object($job) && $job->id == $async_id ) {
                $jm->delete_job( $job );
            }
        }

        $this['status'] = NMS_JOBSTATUS_ABORTED;
        $this->save();
    }


    /**
     * This reverts a job to unstarted status.
     *
     * Any cgjobmgr job is deleted, and the mails for this job are deleted.  If this job is then started
     * it is possibl that the list members receive duplicated mails.
     */
    public function reset()
    {
        // delete all emails
        if( $this['id'] < 1 ) throw new Exception($mod->Lang('error_jobnotsaved'));

        $db = cmsms()->GetDb();
        $query = 'DELETE FROM '.NMS_MAILS_TABLE.' WHERE jobid = ?';
        $db->Execute($query);

        if( isset($this['async_id']) ) {
            // delete the cgjobmgr job
            $async_id = $this['async_id'];
            if( $async_id > 0 ) {
                $jm = \CMSMS\Async\JobManager::get_instance();
                $job = $jm->load_job( $async_id );
                if( is_object($job) && $job->id == $async_id ) {
                    // prevent recursion.
                    $jm->delete_job( $job );

                }
                unset($this['async_id']);
            }
        }

        // set status to unstarted
        // clear dates
        $this['status'] = NMS_JOBSTATUS_UNSTARTED;
        $this->_data['started'] = null;
        $this->_data['finished'] = null;
        $this->save();
    }
} // end of class
