<?php
/*
 *  $Id: Sluggable.php 10831 2013-05-29 19:32:17Z btowles $
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.doctrine-project.org>.
 */

/**
 * Easily create a slug for each record based on a specified set of fields
 *
 * @package     Doctrine
 * @subpackage  Template
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.doctrine-project.org
 * @since       1.0
 * @version     $Revision$
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 */
class Doctrine_Template_Listener_Sluggable extends Doctrine_Record_Listener
{
    /**
     * Array of sluggable options
     *
     * @var string
     */
    protected $_options = array();

    /**
     * __construct
     *
     * @param string $array
     * @return void
     */
    public function __construct(array $options)
    {
        $this->_options = $options;
    }

    /**
     * Set the slug value automatically when a record is inserted
     *
     * @param Doctrine_Event $event
     * @return void
     */
    public function preInsert(Doctrine_Event $event)
    {
        $record = $event->getInvoker();
        $name = $record->getTable()->getFieldName($this->_options['name']);

        if ( ! $record->$name) {
            $record->$name = $this->buildSlugFromFields($record);
        }
    }

    /**
     * Set the slug value automatically when a record is updated if the options are configured
     * to allow it
     *
     * @param Doctrine_Event $event
     * @return void
     */
    public function preUpdate(Doctrine_Event $event)
    {
        if (false !== $this->_options['unique']) {
            $record = $event->getInvoker();
            $name = $record->getTable()->getFieldName($this->_options['name']);

            if ( ! $record->$name || (
                false !== $this->_options['canUpdate'] &&
                ! array_key_exists($name, $record->getModified())
            )) {
                $record->$name = $this->buildSlugFromFields($record);
            } else if ( ! empty($record->$name) &&
                false !== $this->_options['canUpdate'] &&
                array_key_exists($name, $record->getModified()
            )) {
                $record->$name = $this->buildSlugFromSlugField($record);
            }
        }
    }

    /**
     * Generate the slug for a given Doctrine_Record based on the configured options
     *
     * @param Doctrine_Record $record
     * @return string $slug
     */
    protected function buildSlugFromFields($record)
    {
        if (empty($this->_options['fields'])) {
            if (is_callable($this->_options['provider'])) {
            	$value = call_user_func($this->_options['provider'], $record);
            } else if (method_exists($record, 'getUniqueSlug')) {
                $value = $record->getUniqueSlug($record);
            } else {
                $value = (string) $record;
            }
        } else {
            $value = '';
            foreach ($this->_options['fields'] as $field) {
                $value .= $record->$field . ' ';
            }
            $value = substr($value, 0, -1);
        }

    	if ($this->_options['unique'] === true) {
    		return $this->getUniqueSlug($record, $value);
    	}

        return call_user_func_array($this->_options['builder'], array($value, $record));
    }

    /**
     * Generate the slug for a given Doctrine_Record slug field
     *
     * @param Doctrine_Record $record
     * @return string $slug
     */
    protected function buildSlugFromSlugField($record)
    {
        $name = $record->getTable()->getFieldName($this->_options['name']);
        $value = $record->$name;

        if ($this->_options['unique'] === true) {
            return $this->getUniqueSlug($record, $value);
        }

        return call_user_func_array($this->_options['builder'], array($value, $record));
    }

    /**
     * Creates a unique slug for a given Doctrine_Record. This function enforces the uniqueness by
     * incrementing the values with a postfix if the slug is not unique
     *
     * @param Doctrine_Record $record
     * @param string $slugFromFields
     * @return string $slug
     */
    public function getUniqueSlug($record, $slugFromFields)
    {
        /* fix for use with Column Aggregation Inheritance */
        if ($record->getTable()->getOption('inheritanceMap')) {
          $parentTable = $record->getTable()->getOption('parents');
          $i = 0;
          // Be sure that you do not instanciate an abstract class;
          $reflectionClass = new ReflectionClass($parentTable[$i]);
          while ($reflectionClass->isAbstract()) {
            $i++;
            $reflectionClass = new ReflectionClass($parentTable[$i]);
          }
          $table = Doctrine_Core::getTable($parentTable[$i]);
        } else {
          $table = $record->getTable();
        }

        $name = $table->getFieldName($this->_options['name']);
        $proposal =  call_user_func_array($this->_options['builder'], array($slugFromFields, $record));
        $slug = $proposal;

        $whereString = 'r.' . $name . ' LIKE ?';
        $whereParams = array($proposal.'%');

        if ($record->exists()) {
            $identifier = $record->identifier();
            $whereString .= ' AND r.' . implode(' != ? AND r.', $table->getIdentifierColumnNames()) . ' != ?';
            $whereParams = array_merge($whereParams, array_values($identifier));
        }

        foreach ($this->_options['uniqueBy'] as $uniqueBy) {
            if (is_null($record->$uniqueBy)) {
                $whereString .= ' AND r.'.$uniqueBy.' IS NULL';
            } else {
                $whereString .= ' AND r.'.$uniqueBy.' = ?';
                $value = $record->$uniqueBy;
                if ($value instanceof Doctrine_Record) {
                    $value = current((array) $value->identifier());
                }
                $whereParams[] =  $value;
            }
        }

        // Disable indexby to ensure we get all records
        $originalIndexBy = $table->getBoundQueryPart('indexBy');
        $table->bindQueryPart('indexBy', null);

        $query = $table->createQuery('r')
            ->select('r.' . $name)
            ->where($whereString , $whereParams)
            ->setHydrationMode(Doctrine_Core::HYDRATE_ARRAY);

        // We need to introspect SoftDelete to check if we are not disabling unique records too
        if ($table->hasTemplate('Doctrine_Template_SoftDelete')) {
	        $softDelete = $table->getTemplate('Doctrine_Template_SoftDelete');

	        // we have to consider both situations here
            if ($softDelete->getOption('type') == 'boolean') {
                $conn = $query->getConnection();

                $query->addWhere(
                    '(r.' . $softDelete->getOption('name') . ' = ' . $conn->convertBooleans(true) .
                    ' OR r.' . $softDelete->getOption('name') . ' = ' . $conn->convertBooleans(false) . ')'
                );
            } else {
                $query->addWhere('(r.' . $softDelete->getOption('name') . ' IS NOT NULL OR r.' . $softDelete->getOption('name') . ' IS NULL)');
            }
        }

        $similarSlugResult = $query->execute();
        $query->free();

        // Change indexby back
        $table->bindQueryPart('indexBy', $originalIndexBy);

        $similarSlugs = array();
        foreach ($similarSlugResult as $key => $value) {
            $similarSlugs[$key] = strtolower($value[$name]);
        }

        $i = 1;
        while (in_array(strtolower($slug), $similarSlugs)) {
            $slug = call_user_func_array($this->_options['builder'], array($proposal.'-'.$i, $record));
            $i++;
        }

        // If slug is longer then the column length then we need to trim it
        // and try to generate a unique slug again
        $length = $table->getFieldLength($this->_options['name']);
        if (strlen($slug) > $length) {
            $slug = substr($slug, 0, $length - (strlen($i) + 1));
            $slug = $this->getUniqueSlug($record, $slug);
        }

        return  $slug;
    }
}