<?php

/*
 * This file is part of Phraseanet
 *
 * (c) 2005-2016 Alchemy
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

use Alchemy\Phrasea\Application;
use Alchemy\Phrasea\Collection\CollectionRepositoryRegistry;
use Alchemy\Phrasea\Core\Connection\ConnectionSettings;
use Alchemy\Phrasea\Core\PhraseaTokens;
use Alchemy\Phrasea\Core\Thumbnail\ThumbnailedElement;
use Alchemy\Phrasea\Core\Version\DataboxVersionRepository;
use Alchemy\Phrasea\Databox\DataboxRepository;
use Alchemy\Phrasea\Databox\Record\RecordRepository;
use Alchemy\Phrasea\Databox\SubdefGroup;
use Alchemy\Phrasea\Exception\InvalidArgumentException;
use Alchemy\Phrasea\Model\Entities\User;
use Alchemy\Phrasea\Status\StatusStructure;
use Alchemy\Phrasea\Status\StatusStructureFactory;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver\Statement;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Translation\TranslatorInterface;

use Alchemy\Phrasea\Core\Event\Databox\DataboxEvents;
use Alchemy\Phrasea\Core\Event\Databox\CreatedEvent;
use Alchemy\Phrasea\Core\Event\Databox\DeletedEvent;
use Alchemy\Phrasea\Core\Event\Databox\MountedEvent;
use Alchemy\Phrasea\Core\Event\Databox\ReindexAskedEvent;
use Alchemy\Phrasea\Core\Event\Databox\StructureChangedEvent;
use Alchemy\Phrasea\Core\Event\Databox\ThesaurusChangedEvent;
use Alchemy\Phrasea\Core\Event\Databox\TouChangedEvent;
use Alchemy\Phrasea\Core\Event\Databox\UnmountedEvent;


class databox extends base implements ThumbnailedElement
{

    const BASE_TYPE = self::DATA_BOX;
    const CACHE_META_STRUCT = 'meta_struct';
    const CACHE_THESAURUS = 'thesaurus';
    const CACHE_COLLECTIONS = 'collections';
    const CACHE_STRUCTURE = 'structure';
    const PIC_PDF = 'logopdf';
    const CACHE_CGUS = 'cgus';

    /** @var array */
    protected static $_xpath_thesaurus = [];
    /** @var array */
    protected static $_dom_thesaurus = [];
    /** @var array */
    protected static $_thesaurus = [];

    /** @var SimpleXMLElement */
    protected static $_sxml_thesaurus = [];

    /**
     * @param Application $app
     * @param Connection  $databoxConnection
     * @param SplFileInfo $template
     * @return databox
     * @throws \Doctrine\DBAL\DBALException
     */
    public static function create(Application $app, Connection $databoxConnection, \SplFileInfo $template)
    {
        $rp = $template->getRealPath();
        if (!$rp || !file_exists($rp)) {
            throw new \InvalidArgumentException(sprintf('Databox template "%s" not found.', $template->getFilename()));
        }

        $host = $databoxConnection->getHost();
        $port = $databoxConnection->getPort();
        $dbname = $databoxConnection->getDatabase();
        $user = $databoxConnection->getUsername();
        $password = $databoxConnection->getPassword();

        $appbox = $app->getApplicationBox();

        try {
            $sql = 'CREATE DATABASE `' . $dbname . '` CHARACTER SET utf8 COLLATE utf8_unicode_ci';
            $stmt = $databoxConnection->prepare($sql);
            $stmt->execute();
            $stmt->closeCursor();
        } catch (\Exception $e) {

        }

        $sql = 'USE `' . $dbname . '`';
        $stmt = $databoxConnection->prepare($sql);
        $stmt->execute();
        $stmt->closeCursor();

        $app['orm.add']([
            'host'     => $host,
            'port'     => $port,
            'dbname'   => $dbname,
            'user'     => $user,
            'password' => $password
        ]);

        phrasea::reset_sbasDatas($app['phraseanet.appbox']);

        /** @var DataboxRepository $databoxRepository */
        $databoxRepository = $app['repo.databoxes'];
        $databox = $databoxRepository->create($host, $port, $user, $password, $dbname);

        $appbox->delete_data_from_cache(appbox::CACHE_LIST_BASES);

        $databox->insert_datas();
        $databox->setNewStructure(
            $template, $app['conf']->get(['main', 'storage', 'subdefs'])
        );

        $app['dispatcher']->dispatch(DataboxEvents::CREATED, new CreatedEvent($databox));

        return $databox;
    }

    /**
     *
     * @param  Application $app
     * @param  string      $host
     * @param  int         $port
     * @param  string      $user
     * @param  string      $password
     * @param  string      $dbname
     * @return databox
     */
    public static function mount(Application $app, $host, $port, $user, $password, $dbname)
    {
        $app['db.provider']([
            'host'     => $host,
            'port'     => $port,
            'user'     => $user,
            'password' => $password,
            'dbname'   => $dbname,
        ])->connect();

        /** @var DataboxRepository $databoxRepository */
        $databoxRepository = $app['repo.databoxes'];
        $databox = $databoxRepository->mount($host, $port, $user, $password, $dbname);

        $databox->delete_data_from_cache(databox::CACHE_COLLECTIONS);
        $app->getApplicationBox()->delete_data_from_cache(appbox::CACHE_LIST_BASES);

        phrasea::reset_sbasDatas($app['phraseanet.appbox']);
        cache_databox::update($app, $databox->get_sbas_id(), 'structure');

        $app['dispatcher']->dispatch(DataboxEvents::MOUNTED, new MountedEvent($databox));

        return $databox;
    }

    public static function get_available_dcfields()
    {
        return [
            databox_Field_DCESAbstract::Contributor => new databox_Field_DCES_Contributor(),
            databox_Field_DCESAbstract::Coverage    => new databox_Field_DCES_Coverage(),
            databox_Field_DCESAbstract::Creator     => new databox_Field_DCES_Creator(),
            databox_Field_DCESAbstract::Date        => new databox_Field_DCES_Date(),
            databox_Field_DCESAbstract::Description => new databox_Field_DCES_Description(),
            databox_Field_DCESAbstract::Format      => new databox_Field_DCES_Format(),
            databox_Field_DCESAbstract::Identifier  => new databox_Field_DCES_Identifier(),
            databox_Field_DCESAbstract::Language    => new databox_Field_DCES_Language(),
            databox_Field_DCESAbstract::Publisher   => new databox_Field_DCES_Publisher(),
            databox_Field_DCESAbstract::Relation    => new databox_Field_DCES_Relation(),
            databox_Field_DCESAbstract::Rights      => new databox_Field_DCES_Rights(),
            databox_Field_DCESAbstract::Source      => new databox_Field_DCES_Source(),
            databox_Field_DCESAbstract::Subject     => new databox_Field_DCES_Subject(),
            databox_Field_DCESAbstract::Title       => new databox_Field_DCES_Title(),
            databox_Field_DCESAbstract::Type        => new databox_Field_DCES_Type()
        ];
    }

    /**
     *
     * @param  int $sbas_id
     * @return string
     */
    public static function getPrintLogo($sbas_id)
    {
        $out = '';
        if (is_file(($filename = __DIR__ . '/../../config/minilogos/'.\databox::PIC_PDF.'_' . $sbas_id . '.jpg')))
            $out = file_get_contents($filename);

        return $out;
    }

    /**
     * @param TranslatorInterface $translator
     * @param  string             $structure
     * @return Array
     */
    public static function get_structure_errors(TranslatorInterface $translator, $structure)
    {
        $sx_structure = simplexml_load_string($structure);

        $subdefgroup = $sx_structure->subdefs[0];
        $AvSubdefs = [];

        $errors = [];

        foreach ($subdefgroup as $k => $subdefs) {
            $subdefgroup_name = trim((string) $subdefs->attributes()->name);

            if ($subdefgroup_name == '') {
                $errors[] = $translator->trans('ERREUR : TOUTES LES BALISES subdefgroup necessitent un attribut name');
                continue;
            }

            if ( ! isset($AvSubdefs[$subdefgroup_name]))
                $AvSubdefs[$subdefgroup_name] = [];

            foreach ($subdefs as $sd) {
                $sd_name = trim(mb_strtolower((string) $sd->attributes()->name));
                $sd_class = trim(mb_strtolower((string) $sd->attributes()->class));
                if ($sd_name == '' || isset($AvSubdefs[$subdefgroup_name][$sd_name])) {
                    $errors[] = $translator->trans('ERREUR : Les name de subdef sont uniques par groupe de subdefs et necessaire');
                    continue;
                }
                if ( ! in_array($sd_class, ['thumbnail', 'preview', 'document'])) {
                    $errors[] = $translator->trans('ERREUR : La classe de subdef est necessaire et egal a "thumbnail","preview" ou "document"');
                    continue;
                }
                $AvSubdefs[$subdefgroup_name][$sd_name] = $sd;
            }
        }

        return $errors;
    }

    public static function purge()
    {
        self::$_xpath_thesaurus = self::$_dom_thesaurus = self::$_thesaurus = self::$_sxml_thesaurus = [];
    }

    /** @var int */
    protected $id;
    /** @var string */
    protected $structure;
    /** @var array */
    protected $_xpath_structure;
    /** @var DOMDocument */
    protected $_dom_structure;
    /** @var DOMDocument */
    protected $_dom_cterms;
    /** @var SimpleXMLElement */
    protected $_sxml_structure;
    /** @var databox_descriptionStructure */
    protected $meta_struct;
    /** @var databox_subdefsStructure */
    protected $subdef_struct;
    protected $thesaurus;
    protected $cterms;
    protected $cgus;
    /** @var DataboxRepository */
    private $databoxRepository;
    /** @var RecordRepository */
    private $recordRepository;
    /** @var string[]  */
    private $labels = [];
    /** @var int */
    private $ord;
    /** @var string */
    private $viewname;

    /**
     * @param Application $app
     * @param int $sbas_id
     * @param DataboxRepository $databoxRepository
     * @param array $row
     */
    public function __construct(Application $app, $sbas_id, DataboxRepository $databoxRepository, array $row)
    {
        assert(is_int($sbas_id));
        assert($sbas_id > 0);

        $this->databoxRepository = $databoxRepository;
        $this->id = $sbas_id;

        $connectionConfigs = phrasea::sbas_params($app);

        if (! isset($connectionConfigs[$sbas_id])) {
            throw new NotFoundHttpException(sprintf('databox %d not found', $sbas_id));
        }

        $connectionConfig = $connectionConfigs[$sbas_id];
        $connection = $app['db.provider']($connectionConfig);

        $connectionSettings = new ConnectionSettings(
            $connectionConfig['host'],
            $connectionConfig['port'],
            $connectionConfig['dbname'],
            $connectionConfig['user'],
            $connectionConfig['password']
        );

        $versionRepository = new DataboxVersionRepository($connection);

        parent::__construct($app, $connection, $connectionSettings, $versionRepository);

        $this->loadFromRow($row);
    }

    public function setNewStructure(\SplFileInfo $data_template, $path_doc)
    {
        if ( ! file_exists($data_template->getPathname())) {
            throw new \InvalidArgumentException(sprintf('File %s does not exists'));
        }

        $contents = file_get_contents($data_template->getPathname());

        $contents = str_replace(
            ["{{basename}}", "{{datapathnoweb}}"]
            , [$this->connectionSettings->getDatabaseName(), rtrim($path_doc, '/').'/']
            , $contents
        );

        $dom_doc = new DOMDocument();
        $dom_doc->loadXML($contents);
        $this->saveStructure($dom_doc);

        $this->feed_meta_fields();

        return $this;
    }

    /**
     *
     * @param  DOMDocument $dom_struct
     * @return databox
     */
    public function saveStructure(DOMDocument $dom_struct)
    {
        $old_structure = $this->get_dom_structure();

        $dom_struct->documentElement
            ->setAttribute("modification_date", $now = date("YmdHis"));

        $sql = "UPDATE pref SET value= :structure, updated_on= :now WHERE prop='structure'";

        $this->structure = $dom_struct->saveXML();

        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute(
            [
                ':structure' => $this->structure,
                ':now'       => $now
            ]
        );
        $stmt->closeCursor();

        $this->_sxml_structure = $this->_dom_structure = $this->_xpath_structure = null;

        $this->meta_struct = null;

        $this->get_appbox()->delete_data_from_cache(appbox::CACHE_LIST_BASES);
        $this->delete_data_from_cache(self::CACHE_STRUCTURE);
        $this->delete_data_from_cache(self::CACHE_META_STRUCT);

        cache_databox::update($this->app, $this->id, 'structure');

        $this->databoxRepository->save($this);

        $this->app['dispatcher']->dispatch(
            DataboxEvents::STRUCTURE_CHANGED,
            new StructureChangedEvent(
                $this,
                [
                    'dom_before'=>$old_structure
                ]
            )
        );

        return $this;
    }

    /**
     * @return DOMDocument
     */
    public function get_dom_structure()
    {
        if ($this->_dom_structure) {
            return $this->_dom_structure;
        }

        $structure = $this->get_structure();

        $dom = new DOMDocument();

        $dom->standalone = true;
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = true;

        if ($structure && $dom->loadXML($structure) !== false)
            $this->_dom_structure = $dom;
        else
            $this->_dom_structure = false;

        return $this->_dom_structure;
    }

    /**
     * @return string
     */
    public function get_structure()
    {
        if ($this->structure) {
            return $this->structure;
        }

        $this->structure = $this->retrieve_structure();

        return $this->structure;
    }

    public function feed_meta_fields()
    {
        $sxe = $this->get_sxml_structure();

        foreach ($sxe->description->children() as $fname => $field) {
            $dom_struct = $this->get_dom_structure();
            $xp_struct = $this->get_xpath_structure();
            $fname = (string) $fname;
            $src = trim(isset($field['src']) ? str_replace('/rdf:RDF/rdf:Description/', '', $field['src']) : '');

            $meta_id = isset($field['meta_id']) ? $field['meta_id'] : null;
            if ( ! is_null($meta_id))
                continue;

            $nodes = $xp_struct->query('/record/description/' . $fname);
            if ($nodes->length > 0) {
                $nodes->item(0)->parentNode->removeChild($nodes->item(0));
            }
            $this->saveStructure($dom_struct);

            $type = isset($field['type']) ? $field['type'] : 'string';
            $type = in_array($type
                , [
                    databox_field::TYPE_DATE
                    , databox_field::TYPE_NUMBER
                    , databox_field::TYPE_STRING
                ]
            ) ? $type : databox_field::TYPE_STRING;

            $multi = isset($field['multi']) ? (Boolean) (string) $field['multi'] : false;

            $meta_struct_field = databox_field::create($this->app, $this, $fname);
            $meta_struct_field
                ->set_multi($multi)
                ->set_readonly(isset($field['readonly']) ? (string) $field['readonly'] : 0)
                ->set_indexable(isset($field['index']) ? (string) $field['index'] : '1')
                ->set_separator(isset($field['separator']) ? (string) $field['separator'] : '')
                ->set_required((isset($field['required']) && (string) $field['required'] == 1))
                ->set_business((isset($field['business']) && (string) $field['business'] == 1))
                ->set_aggregable((isset($field['aggregable']) ? (string) $field['aggregable'] : 0))
                ->set_type($type)
                ->set_tbranch(isset($field['tbranch']) ? (string) $field['tbranch'] : '')
                ->set_generate_cterms((isset($field['generate_cterms']) && (string) $field['generate_cterms'] == 1))
                ->set_gui_editable((!isset($field['gui_editable']) || (isset($field['gui_editable']) && (string) $field['gui_editable'] == 1)))
                ->set_gui_visible((!isset($field['gui_visible']) || (isset($field['gui_visible']) && (string) $field['gui_visible'] == 1)))
                ->set_printable((!isset($field['printable']) || (isset($field['printable']) && (string) $field['printable'] == 1)))
                ->set_input_disable((isset($field['input_disable']) && (string) $field['input_disable'] == 1))
                ->set_thumbtitle(isset($field['thumbtitle']) ? (string) $field['thumbtitle'] : (isset($field['thumbTitle']) ? $field['thumbTitle'] : '0'))
                ->set_report(isset($field['report']) ? (string) $field['report'] : '1')
                ->save();

            try {
                $meta_struct_field->set_tag(\databox_field::loadClassFromTagName($src))->save();
            } catch (\Exception $e) {
            }
        }

        return $this;
    }

    /**
     *
     * @return SimpleXMLElement
     */
    public function get_sxml_structure()
    {
        if ($this->_sxml_structure) {
            return $this->_sxml_structure;
        }

        $structure = $this->get_structure();

        if ($structure && false !== $tmp = simplexml_load_string($structure))
            $this->_sxml_structure = $tmp;
        else
            $this->_sxml_structure = false;

        return $this->_sxml_structure;
    }

    /**
     * @return DOMXpath
     */
    public function get_xpath_structure()
    {
        if ($this->_xpath_structure) {
            return $this->_xpath_structure;
        }

        $dom_doc = $this->get_dom_structure();

        if ($dom_doc && ($tmp = new DOMXpath($dom_doc)) !== false)
            $this->_xpath_structure = $tmp;
        else
            $this->_xpath_structure = false;

        return $this->_xpath_structure;
    }

    /**
     * @return RecordRepository
     */
    public function getRecordRepository()
    {
        if (null === $this->recordRepository) {
            $this->recordRepository = $this->app['repo.records.factory']($this);
        }

        return $this->recordRepository;
    }

    public function get_ord()
    {
        return $this->ord;
    }

    public function getRootIdentifier()
    {
        return $this->get_sbas_id();
    }

    /**
     * Returns current sbas_id
     *
     * @return int
     */
    public function get_sbas_id()
    {
        return $this->id;
    }

    public function updateThumbnail($thumbnailType, File $file = null)
    {
        $this->delete_data_from_cache('printLogo');
    }

    public function delete_data_from_cache($option = null)
    {
        switch ($option) {
            case self::CACHE_CGUS:
                $this->cgus = null;
                break;
            case self::CACHE_META_STRUCT:
                $this->meta_struct = null;
                break;
            case self::CACHE_STRUCTURE:
                $this->_dom_structure = $this->_xpath_structure = $this->structure = $this->_sxml_structure = null;
                break;
            case self::CACHE_THESAURUS:
                $this->thesaurus = null;
                unset(self::$_dom_thesaurus[$this->id]);
                break;
            default:
                break;
        }
        parent::delete_data_from_cache($option);
    }

    /*
     * trivial cache to speed-up get_collections() which does sql
     */
    private $_collection_unique_ids = null;
    private $_collections = null;

    public function clearCache($what)
    {
        switch($what) {
            case self::CACHE_COLLECTIONS:
                $this->_collection_unique_ids = $this->_collections = null;
                break;
        }
    }

    /**
     * @return int[]
     */
    public function get_collection_unique_ids()
    {
        if($this->_collection_unique_ids === null) {
            $this->_collection_unique_ids = [];

            foreach ($this->get_collections() as $collection) {
                $this->_collection_unique_ids[] = $collection->get_base_id();
            }
        }

        return $this->_collection_unique_ids;
    }

    /**
     * @return collection[]
     */
    public function get_collections()
    {
        if($this->_collections === null) {
            /** @var CollectionRepositoryRegistry $repositoryRegistry */
            $repositoryRegistry = $this->app['repo.collections-registry'];
            $repository = $repositoryRegistry->getRepositoryByDatabox($this->get_sbas_id());

           $this->_collections = array_filter($repository->findAll(), function (collection $collection) {
               return $collection->is_active();
           });
        }

        return $this->_collections;
    }


    /**
     * @return collection|null
     */
    public function getTrashCollection()
    {
        foreach($this->get_collections() as $collection) {
            if($collection->get_name() === '_TRASH_') {
                return $collection;
            }
        }
        return null;
    }

    /**
     *
     * @param  int            $record_id
     * @param  int            $number
     * @return record_adapter
     */
    public function get_record($record_id, $number = null)
    {
        return new record_adapter($this->app, $this->id, $record_id, $number);
    }

    public function get_label($code, $substitute = true)
    {
        if (!array_key_exists($code, $this->labels)) {
            throw new InvalidArgumentException(sprintf('Code %s is not defined', $code));
        }

        if ($substitute) {
            return isset($this->labels[$code]) ? $this->labels[$code] : $this->get_viewname();
        } else {
            return $this->labels[$code];
        }
    }

    public function get_viewname()
    {
        return $this->viewname ? : $this->connectionSettings->getDatabaseName();
    }

    public function set_viewname($viewname)
    {
        $sql = 'UPDATE sbas SET viewname = :viewname WHERE sbas_id = :sbas_id';

        $stmt = $this->get_appbox()->get_connection()->prepare($sql);
        $stmt->execute([':viewname' => $viewname, ':sbas_id' => $this->id]);
        $stmt->closeCursor();

        $this->get_appbox()->delete_data_from_cache(appbox::CACHE_LIST_BASES);
        cache_databox::update($this->app, $this->id, 'structure');

        $this->viewname = $viewname;
        $this->databoxRepository->save($this);

        return $this;
    }

    /**
     * @return appbox
     */
    public function get_appbox()
    {
        return $this->app->getApplicationBox();
    }

    public function set_label($code, $label)
    {
        if (!array_key_exists($code, $this->labels)) {
            throw new InvalidArgumentException(sprintf('Code %s is not defined', $code));
        }

        $sql = "UPDATE sbas SET label_$code = :label
            WHERE sbas_id = :sbas_id";
        $stmt = $this->get_appbox()->get_connection()->prepare($sql);
        $stmt->execute([':label' => $label, ':sbas_id'   => $this->id]);
        $stmt->closeCursor();

        $this->labels[$code] = $label;

        $this->databoxRepository->save($this);

        phrasea::reset_sbasDatas($this->app['phraseanet.appbox']);

        return $this;
    }

    /**
     * @return StatusStructure
     */
    public function getStatusStructure()
    {
        /** @var StatusStructureFactory $structureFactory */
        $structureFactory = $this->app['factory.status-structure'];

        return $structureFactory->getStructure($this);
    }

    public function get_record_details($sort)
    {
        $sql = "SELECT record.coll_id, ISNULL(coll.coll_id) AS lostcoll,
                        COALESCE(asciiname, CONCAT('_',record.coll_id)) AS asciiname, name,
                        SUM(1) AS n, SUM(size) AS siz FROM (record, subdef)
                    LEFT JOIN coll ON record.coll_id=coll.coll_id
                    WHERE record.record_id = subdef.record_id
                    GROUP BY record.coll_id, name
          UNION
          SELECT coll.coll_id, 0, asciiname, '_' AS name, 0 AS n, 0 AS siz
            FROM coll LEFT JOIN record ON record.coll_id=coll.coll_id
            WHERE ISNULL(record.coll_id)
                    GROUP BY record.coll_id, name";

        if ($sort == "obj") {
            $sortk1 = "name";
            $sortk2 = "asciiname";
        } else {
            $sortk1 = "asciiname";
            $sortk2 = "name";
        }

        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        foreach ($rs as $rowbas) {
            if ( ! isset($trows[$rowbas[$sortk1]]))
                $trows[$rowbas[$sortk1]] = [];
            $trows[$rowbas[$sortk1]][$rowbas[$sortk2]] = [
                "coll_id"   => $rowbas["coll_id"],
                "asciiname" => $rowbas["asciiname"],
                "lostcoll"  => $rowbas["lostcoll"],
                "name"      => $rowbas["name"],
                "n"         => $rowbas["n"],
                "siz"       => $rowbas["siz"]
            ];
        }

        ksort($trows);
        foreach ($trows as $kgrp => $vgrp)
            ksort($trows[$kgrp]);

        return $trows;
    }

    public function get_record_amount()
    {
        $sql = "SELECT COUNT(record_id) AS n FROM record";
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $rowbas = $stmt->fetch(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        $amount = $rowbas ? (int) $rowbas["n"] : null;

        return $amount;
    }

    public function get_counts()
    {
        $mask = PhraseaTokens::MAKE_SUBDEF | PhraseaTokens::TO_INDEX | PhraseaTokens::INDEXING; // we only care about those "jetons"
        $sql = "SELECT type, jeton & (".$mask.") AS status, SUM(1) AS n FROM record GROUP BY type, (jeton & ".$mask.")";
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        $ret = [
            'records'             => 0,
            'records_indexed'     => 0,    // jetons = 0;0
            'records_to_index'    => 0,    // jetons = 0;1
            'records_not_indexed' => 0,    // jetons = 1;0
            'records_indexing'    => 0,    // jetons = 1;1
            'subdefs_todo'        => []   // by type "image", "video", ...
        ];
        foreach ($rs as $row) {
            $ret['records'] += ($n = (int)($row['n']));
            $status = $row['status'];
            switch($status & (PhraseaTokens::TO_INDEX | PhraseaTokens::INDEXING)) {
                case 0:
                    $ret['records_indexed'] += $n;
                    break;
                case PhraseaTokens::TO_INDEX:
                    $ret['records_to_index'] += $n;
                    break;
                case PhraseaTokens::INDEXING:
                    $ret['records_not_indexed'] += $n;
                    break;
                case PhraseaTokens::INDEXING | PhraseaTokens::TO_INDEX:
                    $ret['records_indexing'] += $n;
                    break;
            }
            if($status & PhraseaTokens::MAKE_SUBDEF) {
                if(!array_key_exists($row['type'], $ret['subdefs_todo'])) {
                    $ret['subdefs_todo'][$row['type']] = 0;
                }
                $ret['subdefs_todo'][$row['type']] += $n;
            }
        }

        return $ret;
    }

    public function unmount_databox()
    {
        $old_dbname = $this->get_dbname();

        foreach ($this->get_collections() as $collection) {
            $collection->unmount();
        }

        $query = $this->app['phraseanet.user-query'];
        $total = $query->on_sbas_ids([$this->id])
            ->include_phantoms(false)
            ->include_special_users(true)
            ->include_invite(true)
            ->include_templates(true)
            ->get_total();
        $n = 0;
        while ($n < $total) {
            $results = $query->limit($n, 50)->execute()->get_results();
            foreach ($results as $user) {
                $this->app->getAclForUser($user)->delete_data_from_cache(ACL::CACHE_RIGHTS_SBAS);
                $this->app->getAclForUser($user)->delete_data_from_cache(ACL::CACHE_RIGHTS_BAS);
                $this->app->getAclForUser($user)->delete_injected_rights_sbas($this);
            }
            $n+=50;
        }

        foreach ($this->app['repo.story-wz']->findByDatabox($this->app, $this) as $story) {
            $this->app['orm.em']->remove($story);
        }

        foreach ($this->app['repo.basket-elements']->findElementsByDatabox($this) as $element) {
            $this->app['orm.em']->remove($element);
        }

        $this->app['orm.em']->flush();

        $params = [':site_id' => $this->app['conf']->get(['main', 'key'])];

        $sql = 'DELETE FROM clients WHERE site_id = :site_id';
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute($params);
        $stmt->closeCursor();

        $sql = 'DELETE FROM memcached WHERE site_id = :site_id';
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute($params);
        $stmt->closeCursor();

        $sql = "DELETE FROM sbas WHERE sbas_id = :sbas_id";
        $stmt = $this->get_appbox()->get_connection()->prepare($sql);
        $stmt->execute([':sbas_id' => $this->id]);
        $stmt->closeCursor();

        $sql = "DELETE FROM sbasusr WHERE sbas_id = :sbas_id";
        $stmt = $this->get_appbox()->get_connection()->prepare($sql);
        $stmt->execute([':sbas_id' => $this->id]);
        $stmt->closeCursor();

        $this->databoxRepository->unmount($this);
        $this->get_appbox()->delete_data_from_cache(appbox::CACHE_LIST_BASES);

        $this->app['dispatcher']->dispatch(
            DataboxEvents::UNMOUNTED,
            new UnmountedEvent(
                null,
                [
                    'dbname'=>$old_dbname
                ]
            )
        );

        return;
    }

    public function get_base_type()
    {
        return self::BASE_TYPE;
    }

    public function get_cache_key($option = null)
    {
        return 'databox_' . $this->id . '_' . ($option ? $option . '_' : '');
    }

    /**
     * @return databox_descriptionStructure|databox_field[]
     */
    public function get_meta_structure()
    {
        if ($this->meta_struct) {
            return $this->meta_struct;
        }

        /** @var \Alchemy\Phrasea\Databox\Field\DataboxFieldRepository $fieldRepository */
        $fieldRepository = $this->app['repo.fields.factory']($this);

        $this->meta_struct = new databox_descriptionStructure($fieldRepository->findAll(), $this->app['unicode']);

        return $this->meta_struct;
    }

    /**
     * @return databox_subdefsStructure|SubdefGroup[]
     */
    public function get_subdef_structure()
    {
        if (! $this->subdef_struct) {
            $this->subdef_struct = new databox_subdefsStructure($this, $this->app['translator']);
        }

        return $this->subdef_struct;
    }

    public function delete()
    {
        $old_dbname = $this->get_dbname();

        $sql = 'DROP DATABASE `' . $this->get_dbname() . '`';
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $stmt->closeCursor();

        $this->get_appbox()->delete_data_from_cache(appbox::CACHE_LIST_BASES);

        $this->databoxRepository->delete($this);

        $this->app['dispatcher']->dispatch(
            DataboxEvents::DELETED,
            new DeletedEvent(
                null,
                [
                    'dbname'=>$old_dbname
                ]
            )
        );

        return;
    }

    public function get_serialized_server_info()
    {
        return sprintf("%s@%s:%s (MySQL %s)",
            $this->connectionSettings->getDatabaseName(),
            $this->connectionSettings->getHost(),
            $this->connectionSettings->getPort(),
            $this->get_connection()->getWrappedConnection()->getAttribute(\PDO::ATTR_SERVER_VERSION)
        );
    }

    /**
     *
     * @return Array
     */
    public function get_mountable_colls()
    {
        /** @var Connection $conn */
        $conn = $this->get_appbox()->get_connection();
        $colls = [];

        $sql = 'SELECT server_coll_id FROM bas WHERE sbas_id = :sbas_id';
        $stmt = $conn->prepare($sql);
        $stmt->execute([':sbas_id' => $this->id]);
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();
        unset($stmt);

        foreach ($rs as $row) {
            $colls[] = (int) $row['server_coll_id'];
        }

        $mountable_colls = [];

        $builder = $this->get_connection()->createQueryBuilder();
        $builder
            ->select('c.coll_id', 'c.asciiname')
            ->from('coll', 'c');
        if (count($colls) > 0) {
            $builder
                ->where($builder->expr()->notIn('c.coll_id', [':colls']))
                ->setParameter('colls', $colls, Connection::PARAM_INT_ARRAY)
            ;
        }

        /** @var Statement $stmt */
        $stmt = $builder->execute();
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();
        unset($stmt);

        foreach ($rs as $row) {
            $mountable_colls[$row['coll_id']] = $row['asciiname'];
        }

        return $mountable_colls;
    }

    public function get_activable_colls()
    {
        /** @var Connection $conn */
        $conn = $this->get_appbox()->get_connection();
        $base_ids = [];

        $sql = 'SELECT base_id FROM bas WHERE sbas_id = :sbas_id AND active = "0"';
        $stmt = $conn->prepare($sql);
        $stmt->execute([':sbas_id' => $this->id]);
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        foreach ($rs as $row) {
            $base_ids[] = (int) $row['base_id'];
        }

        return $base_ids;
    }

    public function saveCterms(DOMDocument $dom_cterms)
    {
        $dom_cterms->documentElement->setAttribute("modification_date", $now = date("YmdHis"));

        $sql = "UPDATE pref SET value = :xml, updated_on = :date
                WHERE prop='cterms'";

        $this->cterms = $dom_cterms->saveXML();
        $params = [
            ':xml'  => $this->cterms
            , ':date' => $now
        ];

        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute($params);
        $stmt->closeCursor();

        $this->databoxRepository->save($this);

        return $this;
    }

    public function saveThesaurus(DOMDocument $dom_thesaurus)
    {
        $old_thesaurus = $this->get_dom_thesaurus();

        $dom_thesaurus->documentElement->setAttribute("modification_date", $now = date("YmdHis"));
        $this->thesaurus = $dom_thesaurus->saveXML();

        $sql = "UPDATE pref SET value = :xml, updated_on = :date WHERE prop='thesaurus'";
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute([':xml'  => $this->thesaurus, ':date' => $now]);
        $stmt->closeCursor();
        $this->delete_data_from_cache(databox::CACHE_THESAURUS);

        $this->databoxRepository->save($this);

        $this->app['dispatcher']->dispatch(
            DataboxEvents::THESAURUS_CHANGED,
            new ThesaurusChangedEvent(
                $this,
                array(
                    'dom_before'=>$old_thesaurus,
                )
            )
        );

        return $this;
    }

    /**
     * @return DOMDocument
     */
    public function get_dom_thesaurus()
    {
        $sbas_id = $this->id;
        if (isset(self::$_dom_thesaurus[$sbas_id])) {
            return self::$_dom_thesaurus[$sbas_id];
        }

        $thesaurus = $this->get_thesaurus();

        $dom = new DOMDocument();

        if ($thesaurus && false !== $dom->loadXML($thesaurus)) {
            self::$_dom_thesaurus[$sbas_id] = $dom;
        } else {
            self::$_dom_thesaurus[$sbas_id] = false;
            unset($dom);
        }

        return self::$_dom_thesaurus[$sbas_id];
    }

    /**
     * @return string
     */
    public function get_thesaurus()
    {
        try {
            $this->thesaurus = $this->get_data_from_cache(self::CACHE_THESAURUS);

            return $this->thesaurus;
        } catch (\Exception $e) {
            unset($e);
        }

        try {
            $sql = 'SELECT value AS thesaurus FROM pref WHERE prop="thesaurus" LIMIT 1;';
            $stmt = $this->get_connection()->prepare($sql);
            $stmt->execute();
            $row = $stmt->fetch(PDO::FETCH_ASSOC);
            $stmt->closeCursor();
            $this->thesaurus = $row['thesaurus'];
            $this->set_data_to_cache($this->thesaurus, self::CACHE_THESAURUS);
        } catch (\Exception $e) {
            unset($e);
        }

        return $this->thesaurus;
    }

    /**
     *
     * @param  User    $user
     * @return databox
     */
    public function registerAdmin(User $user)
    {
        $conn = $this->get_appbox()->get_connection();

        $this->app->getAclForUser($user)
            ->give_access_to_sbas([$this->id])
            ->update_rights_to_sbas(
                $this->id,
                [
                    \ACL::BAS_MANAGE        => true,
                    \ACL::BAS_MODIFY_STRUCT => true,
                    \ACL::BAS_MODIF_TH      => true,
                    \ACL::BAS_CHUPUB        => true
                ]
            );

        $sql = "SELECT * FROM coll";
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        $sql = "INSERT INTO bas (active, server_coll_id, sbas_id) VALUES ('1', :coll_id, :sbas_id)";
        $stmt = $conn->prepare($sql);

        $base_ids = [];
        foreach ($rs as $row) {
            try {
                $stmt->execute([':coll_id'  => $row['coll_id'], ':sbas_id'  => $this->id]);
                $base_ids[] = $base_id = $conn->lastInsertId();

                if ( ! empty($row['logo'])) {
                    file_put_contents($this->app['root.path'] . '/config/minilogos/' . $base_id, $row['logo']);
                }
            } catch (\Exception $e) {
                unset($e);
            }
        }
        $stmt->closeCursor();

        $this->app->getAclForUser($user)->give_access_to_base($base_ids);

        foreach ($base_ids as $base_id) {
            $this->app->getAclForUser($user)
                ->update_rights_to_base(
                    $base_id,
                    [
                        \ACL::CANPUSH            => true,
                        \ACL::CANCMD             => true,
                        \ACL::CANPUTINALBUM      => true,
                        \ACL::CANDWNLDHD         => true,
                        \ACL::CANDWNLDPREVIEW    => true,
                        \ACL::CANADMIN           => true,
                        \ACL::ACTIF              => true,
                        \ACL::CANREPORT          => true,
                        \ACL::CANADDRECORD       => true,
                        \ACL::CANMODIFRECORD     => true,
                        \ACL::CANDELETERECORD    => true,
                        \ACL::CHGSTATUS          => true,
                        \ACL::IMGTOOLS           => true,
                        \ACL::COLL_MANAGE        => true,
                        \ACL::COLL_MODIFY_STRUCT => true,
                        \ACL::NOWATERMARK        => true
                    ]
                );
        }

        $this->app->getAclForUser($user)->delete_data_from_cache();

        return $this;
    }

    public function clear_logs()
    {
        foreach (['log', 'log_docs', 'log_search', 'log_view', 'log_thumb'] as $table) {
            $this->get_connection()->delete($table, []);
        }

        return $this;
    }

    public function clearCandidates()
    {
        try {
            $domct = $this->get_dom_cterms();

            if ($domct !== false) {
                $changed = false;
                $nodesToDel = [];
                // loop on first level : "fields"
                for($n = $domct->documentElement->firstChild; $n; $n = $n->nextSibling) {
                    if($n->nodeType == XML_ELEMENT_NODE && !($n->getAttribute('delbranch'))){
                        $nodesToDel2 = [];
                        // loop on 2nd level : "terms"
                        for($n2 = $n->firstChild; $n2; $n2 = $n2->nextSibling) {
                            // do not remove "rejected" candidates
                            if(substr($n2->getAttribute('id'), 0, 1) != 'R') {
                                $nodesToDel2[] = $n2;
                            }
                        }
                        foreach($nodesToDel2 as $n2) {
                            $n->removeChild($n2);
                            $changed = true;
                        }
                        // if a field has no more candidates, we can remove it
                        if(!($n->firstChild)) {
                            $nodesToDel[] = $n;
                        }
                    }
                }
                foreach($nodesToDel as $n) {
                    $n->parentNode->removeChild($n);
                    $changed = true;
                }
                if($changed) {
                    $this->saveCterms($domct);
                }
            }
        }
        catch (\Exception $e) {
            // no-op
        }
    }

    public function reindex()
    {
        $this->clearCandidates();
        $this->get_connection()->update('pref', ['updated_on' => '0000-00-00 00:00:00'], ['prop' => 'indexes']);

        // Set TO_INDEX flag on all records
        $sql = "UPDATE record SET jeton = ((jeton & ~ :token_and) | :token_or)";
        $stmt = $this->connection->prepare($sql);
        $stmt->bindValue(':token_and', PhraseaTokens::INDEXING, PDO::PARAM_INT);
        $stmt->bindValue(':token_or',  PhraseaTokens::TO_INDEX, PDO::PARAM_INT);
        $stmt->execute();

        $this->app['dispatcher']->dispatch(
            DataboxEvents::REINDEX_ASKED,
            new ReindexAskedEvent(
                $this
            )
        );

        return $this;
    }

    /**
     * @return DOMXpath
     */
    public function get_xpath_thesaurus()
    {
        $sbas_id = $this->id;
        if (isset(self::$_xpath_thesaurus[$sbas_id])) {
            return self::$_xpath_thesaurus[$sbas_id];
        }

        $DOM_thesaurus = $this->get_dom_thesaurus();

        if ($DOM_thesaurus && ($tmp = new thesaurus_xpath($DOM_thesaurus)) !== false)
            self::$_xpath_thesaurus[$sbas_id] = $tmp;
        else
            self::$_xpath_thesaurus[$sbas_id] = false;

        return self::$_xpath_thesaurus[$sbas_id];
    }

    /**
     * @return SimpleXMLElement
     */
    public function get_sxml_thesaurus()
    {
        $sbas_id = $this->id;
        if (isset(self::$_sxml_thesaurus[$sbas_id])) {
            return self::$_sxml_thesaurus[$sbas_id];
        }

        $thesaurus = $this->get_thesaurus();

        if ($thesaurus && false !== $tmp = simplexml_load_string($thesaurus))
            self::$_sxml_thesaurus[$sbas_id] = $tmp;
        else
            self::$_sxml_thesaurus[$sbas_id] = false;

        return self::$_sxml_thesaurus[$sbas_id];
    }

    /**
     * @return DOMDocument
     */
    public function get_dom_cterms()
    {
        if ($this->_dom_cterms) {
            return $this->_dom_cterms;
        }

        $cterms = $this->get_cterms();

        $dom = new DOMDocument();

        $dom->standalone = true;
        $dom->preserveWhiteSpace = false;
        $dom->formatOutput = true;

        if ($cterms && $dom->loadXML($cterms) !== false)
            $this->_dom_cterms = $dom;
        else
            $this->_dom_cterms = false;

        return $this->_dom_cterms;
    }

    /**
     *
     * @return string
     */
    public function get_cterms()
    {
        if ($this->cterms) {
            return $this->cterms;
        }

        $sql = "SELECT value FROM pref WHERE prop='cterms'";
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        if ($row)
            $this->cterms = $row['value'];

        return $this->cterms;
    }

    public function update_cgus($locale, $terms, $reset_date)
    {
        $old_tou = $this->get_cgus();

        $terms = str_replace(["\r\n", "\n", "\r"], ['', '', ''], strip_tags($terms, '<p><strong><a><ul><ol><li><h1><h2><h3><h4><h5><h6>'));
        $sql = 'UPDATE pref SET value = :terms ';

        if ($reset_date)
            $sql .= ', updated_on=NOW() ';

        $sql .= ' WHERE prop="ToU" AND locale = :locale';

        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute([':terms'    => $terms, ':locale'   => $locale]);
        $stmt->closeCursor();
        $this->cgus = null;
        $this->delete_data_from_cache(self::CACHE_CGUS);

        $this->app['dispatcher']->dispatch(
            DataboxEvents::TOU_CHANGED,
            new TouChangedEvent(
                $this,
                [
                    'tou_before'=>$old_tou,
                ]
            )
        );

        return $this;
    }

    public function get_cgus()
    {
        if ($this->cgus) {
            return $this->cgus;
        }

        $this->load_cgus();

        return $this->cgus;
    }

    public function __sleep()
    {
        $this->_sxml_structure = $this->_dom_structure = $this->_xpath_structure = null;

        $vars = [];

        foreach ($this as $key => $value) {
            if (in_array($key, ['app', 'meta_struct'])) {
                continue;
            }

            $vars[] = $key;
        }

        return $vars;
    }

    public function hydrate(Application $app)
    {
        $this->app = $app;
    }

    /**
     * Tells whether the registration is enable or not.
     *
     * @return boolean
     */
    public function isRegistrationEnabled()
    {
        if (false !== $xml = $this->get_sxml_structure()) {
            foreach ($xml->xpath('/record/caninscript') as $canRegister) {
                if (false !== (Boolean) (string) $canRegister) {
                    return true;
                }
            }
        }

        return false;
    }

    /**
     * matches a email against the auto-register whitelist
     *
     * @param string $email
     * @return null|string  the user-model to apply if the email matches
     */
    public function getAutoregisterModel($email)
    {
        if(!$this->isRegistrationEnabled()) {
            return null;
        }

        $ret = User::USER_AUTOREGISTER; // default if there is no whitelist defined at all

        // try to match against the whitelist
        if( ($xml = $this->get_sxml_structure()) !== false) {
            $wl = $xml->xpath('/record/registration/auto_register/email_whitelist');
            if($wl !== false && count($wl) === 1) {
                $ret = null;    // there IS a whitelist, the email must match

                foreach ($wl[0]->xpath('email') as $element) {
                    if (preg_match($element['pattern'], $email) === 1) {
                        return (string)$element['user_model'];
                    }
                }
            }
        }

        return $ret;
    }


    /**
     * Return an array that can be used to restore databox.
     *
     * @return array
     */
    public function getRawData()
    {
        return [
            'ord' => $this->ord,
            'viewname' => $this->viewname,
            'label_en' => $this->labels['en'],
            'label_fr' => $this->labels['fr'],
            'label_de' => $this->labels['de'],
            'label_nl' => $this->labels['nl'],
        ];
    }

    /**
     * Return the default subdef path
     *
     * @return string
     */
    public function getSubdefStorage(){

        return p4string::addEndSlash($this->app['conf']->get(['main', 'storage', 'subdefs'])).$this->get_dbname()."/subdefs/";
    }

    protected function retrieve_structure()
    {
        try {
            $data = $this->get_data_from_cache(self::CACHE_STRUCTURE);
            if (is_array($data)) {
                return $data;
            }
        } catch (\Exception $e) {

        }

        $structure = null;
        $sql = "SELECT value FROM pref WHERE prop='structure'";
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $row = $stmt->fetch(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        if ($row)
            $structure = $row['value'];
        $this->set_data_to_cache($structure, self::CACHE_STRUCTURE);

        return $structure;
    }

    protected function load_cgus()
    {
        try {
            $this->cgus = $this->get_data_from_cache(self::CACHE_CGUS);

            return $this;
        } catch (\Exception $e) {

        }

        $sql = 'SELECT value, locale, updated_on FROM pref WHERE prop ="ToU"';
        $stmt = $this->get_connection()->prepare($sql);
        $stmt->execute();
        $rs = $stmt->fetchAll(PDO::FETCH_ASSOC);
        $stmt->closeCursor();

        $TOU = [];
        foreach ($rs as $row) {
            $TOU[$row['locale']] = ['updated_on' => $row['updated_on'], 'value' => $row['value']];
        }

        $missing_locale = [];

        $avLanguages = $this->app['locales.available'];
        foreach ($avLanguages as $code => $language) {
            if (!isset($TOU[$code])) {
                $missing_locale[] = $code;
            }
        }

        $TOU = array_intersect_key($TOU, $avLanguages);

        $date_obj = new DateTime();
        $date = $this->app['date-formatter']->format_mysql($date_obj);
        $sql = "INSERT INTO pref (id, prop, value, locale, updated_on, created_on)
              VALUES (null, 'ToU', '', :locale, :date, NOW())";
        $stmt = $this->get_connection()->prepare($sql);
        foreach ($missing_locale as $v) {
            $stmt->execute([':locale' => $v, ':date' => $date]);
            $TOU[$v] = ['updated_on' => $date, 'value' => ''];
        }
        $stmt->closeCursor();
        $this->cgus = $TOU;

        $this->set_data_to_cache($TOU, self::CACHE_CGUS);

        return $this;
    }

    /**
     * @param array $row
     */
    private function loadFromRow(array $row)
    {
        $this->ord = $row['ord'];
        $this->viewname = $row['viewname'];
        $this->labels['fr'] = $row['label_fr'];
        $this->labels['en'] = $row['label_en'];
        $this->labels['de'] = $row['label_de'];
        $this->labels['nl'] = $row['label_nl'];
    }
}
