<?php

require_once __DIR__ . '/page.php';
require_once __DIR__ . '/user_agent.php';

/**
 * A composite page.
 * Wraps a frameset page and adds subframes.
 * The original page will be mostly ignored.
 * Implements the SimplePage interface so as to be interchangeable.
 */
class SimpleFrameset
{
    private $frameset;
    private $frames;
    private $focus;
    private $names;

    /**
     * Stashes the frameset page.
     * Will make use of the browser to fetch the sub frames recursively.
     *
     * @param SimplePage $page        Frameset page.
     */
    public function __construct($page)
    {
        $this->frameset = $page;
        $this->frames   = array();
        $this->focus    = false;
        $this->names    = array();
    }

    /**
     * Adds a parsed page to the frameset.
     *
     * @param SimplePage $page    Frame page.
     * @param string $name        Name of frame in frameset.
     */
    public function addFrame($page, $name = false)
    {
        $this->frames[] = $page;
        if ($name) {
            $this->names[$name] = count($this->frames) - 1;
        }
    }

    /**
     * Replaces existing frame with another.
     * If the frame is nested, then the call is passed down one level.
     *
     * @param array $path        Path of frame in frameset.
     * @param SimplePage $page   Frame source.
     */
    public function setFrame($path, $page)
    {
        $name = array_shift($path);
        if (isset($this->names[$name])) {
            $index = $this->names[$name];
        } else {
            $index = $name - 1;
        }
        if (count($path) === 0) {
            $this->frames[$index] = $page;

            return;
        }
        $this->frames[$index]->setFrame($path, $page);
    }

    /**
     * Accessor for current frame focus.
     * Will be false if no frame has focus. Will have the nested frame focus if any.
     *
     * @return array     Labels or indexes of nested frames.
     */
    public function getFrameFocus()
    {
        if ($this->focus === false) {
            return array();
        }

        return array_merge(
                array($this->getPublicNameFromIndex($this->focus)),
                $this->frames[$this->focus]->getFrameFocus());
    }

    /**
     * Turns an internal array index into the frames list,
     * into a public name, or if none, then a one offset index.
     *
     * @param int $subject    Internal index.
     *
     * @return integer/string     Public name.
     */
    protected function getPublicNameFromIndex($subject)
    {
        foreach ($this->names as $name => $index) {
            if ($subject == $index) {
                return $name;
            }
        }

        return $subject + 1;
    }

    /**
     * Sets the focus by index. The integer index starts from 1.
     * If already focused and the target frame also has frames,
     * then the nested frame will be focused.
     *
     * @param int $choice    Chosen frame.
     *
     * @return bool           True if frame exists.
     */
    public function setFrameFocusByIndex($choice)
    {
        if (is_integer($this->focus)) {
            if ($this->frames[$this->focus]->hasFrames()) {
                return $this->frames[$this->focus]->setFrameFocusByIndex($choice);
            }
        }
        if (($choice < 1) || ($choice > count($this->frames))) {
            return false;
        }
        $this->focus = $choice - 1;

        return true;
    }

    /**
     * Sets the focus by name.
     * If already focused and the target frame also has frames,
     * then the nested frame will be focused.
     *
     * @param string $name    Chosen frame.
     *
     * @return bool        True if frame exists.
     */
    public function setFrameFocus($name)
    {
        if (is_integer($this->focus)) {
            if ($this->frames[$this->focus]->hasFrames()) {
                return $this->frames[$this->focus]->setFrameFocus($name);
            }
        }
        if (in_array($name, array_keys($this->names))) {
            $this->focus = $this->names[$name];

            return true;
        }

        return false;
    }

    /**
     * Clears the frame focus.
     */
    public function clearFrameFocus()
    {
        $this->focus = false;
        $this->clearNestedFramesFocus();
    }

    /**
     * Clears the frame focus for any nested frames.
     */
    protected function clearNestedFramesFocus()
    {
        for ($i = 0; $i < count($this->frames); $i++) {
            $this->frames[$i]->clearFrameFocus();
        }
    }

    /**
     * Test for the presence of a frameset.
     *
     * @return bool        Always true.
     */
    public function hasFrames()
    {
        return true;
    }

    /**
     * Accessor for frames information.
     *
     * @return array/string      Recursive hash of frame URL strings. The key is either a numerical
     * index or the name attribute.
     */
    public function getFrames()
    {
        $report = array();
        for ($i = 0; $i < count($this->frames); $i++) {
            $report[$this->getPublicNameFromIndex($i)] = $this->frames[$i]->getFrames();
        }

        return $report;
    }

    /**
     * Accessor for raw text of either all the pages or the frame in focus.
     *
     * @return string        Raw unparsed content.
     */
    public function getRaw()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getRaw();
        }
        $raw = '';
        for ($i = 0; $i < count($this->frames); $i++) {
            $raw .= $this->frames[$i]->getRaw();
        }

        return $raw;
    }

    /**
     * Accessor for plain text of either all the pages or the frame in focus.
     *
     * @return string        Plain text content.
     */
    public function getText()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getText();
        }
        $raw = '';
        for ($i = 0; $i < count($this->frames); $i++) {
            $raw .= ' ' . $this->frames[$i]->getText();
        }

        return trim($raw);
    }

    /**
     * Accessor for last error.
     *
     * @return string        Error from last response.
     */
    public function getTransportError()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getTransportError();
        }

        return $this->frameset->getTransportError();
    }

    /**
     * Request method used to fetch this frame.
     *
     * @return string      GET, POST or HEAD.
     */
    public function getMethod()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getMethod();
        }

        return $this->frameset->getMethod();
    }

    /**
     * Original resource name.
     *
     * @return SimpleUrl        Current url.
     */
    public function getUrl()
    {
        if (is_integer($this->focus)) {
            $url = $this->frames[$this->focus]->getUrl();
            $url->setTarget($this->getPublicNameFromIndex($this->focus));
        } else {
            $url = $this->frameset->getUrl();
        }

        return $url;
    }

    /**
     * Page base URL.
     *
     * @return SimpleUrl        Current url.
     */
    public function getBaseUrl()
    {
        if (is_integer($this->focus)) {
            $url = $this->frames[$this->focus]->getBaseUrl();
        } else {
            $url = $this->frameset->getBaseUrl();
        }

        return $url;
    }

    /**
     * Expands expandomatic URLs into fully qualified URLs for the frameset page.
     *
     * @param SimpleUrl $url        Relative URL.
     *
     * @return SimpleUrl            Absolute URL.
     */
    public function expandUrl($url)
    {
        return $this->frameset->expandUrl($url);
    }

    /**
     * Original request data.
     *
     * @return mixed              Sent content.
     */
    public function getRequestData()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getRequestData();
        }

        return $this->frameset->getRequestData();
    }

    /**
     * Accessor for current MIME type.
     *
     * @return string    MIME type as string; e.g. 'text/html'
     */
    public function getMimeType()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getMimeType();
        }

        return $this->frameset->getMimeType();
    }

    /**
     * Accessor for last response code.
     *
     * @return int    Last HTTP response code received.
     */
    public function getResponseCode()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getResponseCode();
        }

        return $this->frameset->getResponseCode();
    }

    /**
     * Accessor for last Authentication type. Only valid straight after a challenge (401).
     *
     * @return string    Description of challenge type.
     */
    public function getAuthentication()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getAuthentication();
        }

        return $this->frameset->getAuthentication();
    }

    /**
     * Accessor for last Authentication realm. Only valid straight after a challenge (401).
     *
     * @return string    Name of security realm.
     */
    public function getRealm()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getRealm();
        }

        return $this->frameset->getRealm();
    }

    /**
     * Accessor for outgoing header information.
     *
     * @return string      Header block.
     */
    public function getRequest()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getRequest();
        }

        return $this->frameset->getRequest();
    }

    /**
     * Accessor for raw header information.
     *
     * @return string      Header block.
     */
    public function getHeaders()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getHeaders();
        }

        return $this->frameset->getHeaders();
    }

    /**
     * Accessor for parsed title.
     *
     * @return string     Title or false if no title is present.
     */
    public function getTitle()
    {
        return $this->frameset->getTitle();
    }

    /**
     * Accessor for a list of all fixed links.
     *
     * @return array   List of urls as strings.
     */
    public function getUrls()
    {
        if (is_integer($this->focus)) {
            return $this->frames[$this->focus]->getUrls();
        }
        $urls = array();
        foreach ($this->frames as $frame) {
            $urls = array_merge($urls, $frame->getUrls());
        }

        return array_values(array_unique($urls));
    }

    /**
     * Accessor for URLs by the link label. Label will match regardess of whitespace issues and
     * case.
     *
     * @param string $label    Text of link.
     *
     * @return array           List of links with that label.
     */
    public function getUrlsByLabel($label)
    {
        if (is_integer($this->focus)) {
            return $this->tagUrlsWithFrame(
                    $this->frames[$this->focus]->getUrlsByLabel($label),
                    $this->focus);
        }
        $urls = array();
        foreach ($this->frames as $index => $frame) {
            $urls = array_merge(
                    $urls,
                    $this->tagUrlsWithFrame(
                                $frame->getUrlsByLabel($label),
                                $index));
        }

        return $urls;
    }

    /**
     * Accessor for a URL by the id attribute. If in a frameset then the first link found with that
     * ID attribute is returned only. Focus on a frame if you want one from a specific part of the
     * frameset.
     *
     * @param string $id       Id attribute of link.
     *
     * @return string          URL with that id.
     */
    public function getUrlById($id)
    {
        foreach ($this->frames as $index => $frame) {
            if ($url = $frame->getUrlById($id)) {
                if (! $url->gettarget()) {
                    $url->setTarget($this->getPublicNameFromIndex($index));
                }

                return $url;
            }
        }

        return false;
    }

    /**
     * Attaches the intended frame index to a list of URLs.
     *
     * @param array $urls        List of SimpleUrls.
     * @param string $frame      Name of frame or index.
     *
     * @return array             List of tagged URLs.
     */
    protected function tagUrlsWithFrame($urls, $frame)
    {
        $tagged = array();
        foreach ($urls as $url) {
            if (! $url->getTarget()) {
                $url->setTarget($this->getPublicNameFromIndex($frame));
            }
            $tagged[] = $url;
        }

        return $tagged;
    }

    /**
     * Finds a held form by button label. Will only search correctly built forms.
     *
     * @param SimpleSelector $selector       Button finder.
     *
     * @return SimpleForm                    Form object containing the button.
     */
    public function getFormBySubmit($selector)
    {
        return $this->findForm('getFormBySubmit', $selector);
    }

    /**
     * Finds a held form by image using a selector. Will only search correctly built forms. The
     * first form found either within the focused frame, or across frames, will be the one returned.
     *
     * @param SimpleSelector $selector  Image finder.
     *
     * @return SimpleForm               Form object containing the image.
     */
    public function getFormByImage($selector)
    {
        return $this->findForm('getFormByImage', $selector);
    }

    /**
     * Finds a held form by the form ID. A way of identifying a specific form when we have control
     * of the HTML code. The first form found either within the focused frame, or across frames,
     * will be the one returned.
     *
     * @param string $id     Form label.
     *
     * @return SimpleForm    Form object containing the matching ID.
     */
    public function getFormById($id)
    {
        return $this->findForm('getFormById', $id);
    }

    /**
     * General form finder. Will search all the frames or just the one in focus.
     *
     * @param string $method    Method to use to find in a page.
     * @param string $attribute Label, name or ID.
     *
     * @return SimpleForm    Form object containing the matching ID.
     */
    protected function findForm($method, $attribute)
    {
        if (is_integer($this->focus)) {
            return $this->findFormInFrame(
                    $this->frames[$this->focus],
                    $this->focus,
                    $method,
                    $attribute);
        }
        for ($i = 0; $i < count($this->frames); $i++) {
            $form = $this->findFormInFrame(
                    $this->frames[$i],
                    $i,
                    $method,
                    $attribute);
            if ($form) {
                return $form;
            }
        }

        return;
    }

    /**
     * Finds a form in a page using a form finding method.
     * Will also tag the form with the frame name it belongs in.
     *
     * @param SimplePage $page  Page content of frame.
     * @param int $index    Internal frame representation.
     * @param string $method    Method to use to find in a page.
     * @param string $attribute Label, name or ID.
     *
     * @return SimpleForm       Form object containing the matching ID.
     */
    protected function findFormInFrame($page, $index, $method, $attribute)
    {
        $form = $this->frames[$index]->$method($attribute);
        if (isset($form)) {
            $form->setDefaultTarget($this->getPublicNameFromIndex($index));
        }

        return $form;
    }

    /**
     * Sets a field on each form in which the field is available.
     *
     * @param SimpleSelector $selector    Field finder.
     * @param string $value               Value to set field to.
     *
     * @return bool                    True if value is valid.
     */
    public function setField($selector, $value)
    {
        if (is_integer($this->focus)) {
            $this->frames[$this->focus]->setField($selector, $value);
        } else {
            for ($i = 0; $i < count($this->frames); $i++) {
                $this->frames[$i]->setField($selector, $value);
            }
        }
    }

    /**
     * Accessor for a form element value within a page.
     *
     * @param SimpleSelector $selector    Field finder.
     *
     * @return string/boolean             A string if the field is present, false if unchecked and
     * null if missing.
     */
    public function getField($selector)
    {
        for ($i = 0; $i < count($this->frames); $i++) {
            $value = $this->frames[$i]->getField($selector);
            if (isset($value)) {
                return $value;
            }
        }

        return;
    }
}
