import asyncio
from pathlib import Path
from typing import TYPE_CHECKING, Any, Awaitable, List, Optional, Set, Union

from ordered_set import OrderedSet
from biolib.pyppeteer.pyppeteer import helpers
from biolib.pyppeteer.pyppeteer.connection import CDPSession
from biolib.pyppeteer.pyppeteer.domworld import DOMWorld, WaitTask
from biolib.pyppeteer.pyppeteer.errors import BrowserError, PageError
from biolib.pyppeteer.pyppeteer.jshandle import ElementHandle, JSHandle
from biolib.pyppeteer.pyppeteer.models import JSFunctionArg, WaitTargets
from biolib.pyppeteer.pyppeteer.network_manager import Response

if TYPE_CHECKING:
    from pyppeteer.execution_context import ExecutionContext


class Frame:
    """Frame class.

    Frame objects can be obtained via :attr:`pyppeteer.page.Page.mainFrame`.
    """

    def __init__(
        self, frameManager: 'FrameManager', client: CDPSession, parentFrame: Optional['Frame'], frameId: str
    ) -> None:
        self._frameManager = frameManager
        self._client = client
        self._parentFrame = parentFrame
        self._url = ''
        self._id = frameId
        self._detached = False

        self._loaderId = ''
        self._lifecycleEvents: Set[str] = set()
        self._mainWorld = DOMWorld(frameManager, self, frameManager._timeoutSettings)
        self._secondaryWorld = DOMWorld(frameManager, self, frameManager._timeoutSettings)
        self._childFrames: OrderedSet[Frame] = OrderedSet()
        if self._parentFrame:
            self._parentFrame._childFrames.add(self)

        self._waitTasks: Set[WaitTask] = set()
        if self._parentFrame:
            self._parentFrame._childFrames.add(self)

        self.addScriptTag = self.mainWorld.addScriptTag
        self.addStyleTag = self.mainWorld.addStyleTag
        self.evaluate = self.mainWorld.evaluate
        self.evaluateHandle = self.mainWorld.evaluateHandle
        self.querySelector = self.J = self.mainWorld.querySelector
        self.querySelectorAll = self.JJ = self.mainWorld.querySelectorAll
        self.querySelectorAllEval = self.JJeval = self.mainWorld.querySelectorAllEval
        self.querySelectorEval = self.Jeval = self.mainWorld.querySelectorEval
        self.type = self.mainWorld.type
        self.waitForFunction = self.mainWorld.waitForFunction
        self.xpath = self.Jx = self.mainWorld.xpath

        self.click = self.secondaryWorld.click
        self.focus = self.secondaryWorld.focus
        self.hover = self.secondaryWorld.hover
        self.select = self.secondaryWorld.select
        self.setContent = self.secondaryWorld.setContent
        self.tap = self.secondaryWorld.tap

    @property
    async def executionContext(self) -> Optional['ExecutionContext']:
        return await self.mainWorld.executionContext

    @property
    async def content(self) -> str:
        return await self.secondaryWorld.content

    @property
    async def title(self) -> str:
        return await self.secondaryWorld.title

    async def goto(
        self, url: str, referer: str = None, timeout: float = None, waitUntil: WaitTargets = None
    ) -> Optional[Response]:
        return await self._frameManager.navigateFrame(
            self, url=url, referer=referer, timeout=timeout, waitUntil=waitUntil
        )

    async def waitForNavigation(
        self, timeout: Optional[float] = None, waitUntil: Optional[WaitTargets] = None
    ) -> Optional[Response]:
        return await self._frameManager.waitForFrameNavigation(self, waitUntil=waitUntil, timeout=timeout)

    @property
    def mainWorld(self) -> 'DOMWorld':  # ensure mainWorld not settable
        return self._mainWorld

    @property
    def secondaryWorld(self) -> 'DOMWorld':  # ensure secondaryWorld is not settable
        return self._secondaryWorld

    @property
    def name(self) -> str:
        """Get frame name."""
        return getattr(self, '_name', '')

    @property
    def url(self) -> str:
        """Get url of the frame."""
        return self._url

    @property
    def parentFrame(self) -> Optional['Frame']:
        """Get parent frame.

        If this frame is main frame or detached frame, return ``None``.
        """
        return self._parentFrame

    @property
    def childFrames(self) -> List['Frame']:
        """Get child frames."""
        return list(self._childFrames)

    @property
    def isDetached(self) -> bool:
        """Return ``True`` if this frame is detached.

        Otherwise return ``False``.
        """
        return self._detached

    async def addScriptTag(
        self,
        url: Optional[str] = None,
        path: Optional[Union[str, Path]] = None,
        content: Optional[str] = None,
        type_: str = '',
    ) -> ElementHandle:
        """Add script tag to this frame.

        Details see :meth:`pyppeteer.page.Page.addScriptTag`.
        """
        return await self._mainWorld.addScriptTag(url=url, path=path, content=content, type_=type_)

    async def addStyleTag(
        self, url: Optional[str] = None, path: Optional[Union[str, Path]] = None, content: Optional[str] = None
    ) -> Optional['ElementHandle']:
        return await self._mainWorld.addStyleTag(url=url, path=path, content=content)

    async def focus(self, selector: str) -> None:
        """Focus element which matches ``selector``.

        Details see :meth:`pyppeteer.page.Page.focus`.
        """
        handle = await self.J(selector)
        if not handle:
            raise PageError('No node found for selector: ' + selector)
        await self.evaluate('element => element.focus()', handle)
        await handle.dispose()

    async def hover(self, selector: str) -> None:
        """Mouse hover the element which matches ``selector``.

        Details see :meth:`pyppeteer.page.Page.hover`.
        """
        handle = await self.J(selector)
        if not handle:
            raise PageError('No node found for selector: ' + selector)
        await handle.hover()
        await handle.dispose()

    async def select(self, selector: str, *values: str) -> List[str]:
        """Select options and return selected values.

        Details see :meth:`pyppeteer.page.Page.select`.
        """
        for index, value in values:
            if not isinstance(value, str):
                raise TypeError(f'Values must be string. Found {value} of type {type(value)} at index {index}')
        return await self.querySelectorEval(  # type: ignore
            selector,
            '''
                (element, values) => {
                    if (element.nodeName.toLowerCase() !== 'select')
                        throw new Error('Element is not a <select> element.');

                    const options = Array.from(element.options);
                    element.value = undefined;
                    for (const option of options) {
                        option.selected = values.includes(option.value);
                        if (option.selected && !element.multiple)
                            break;
                    }

                    element.dispatchEvent(new Event('input', { 'bubbles': true }));
                    element.dispatchEvent(new Event('change', { 'bubbles': true }));
                    return options.filter(option => option.selected).map(options => options.value)
                }
        ''',
            *values,
        )

    async def tap(self, selector: str) -> None:
        """Tap the element which matches the ``selector``.

        Details see :meth:`pyppeteer.page.Page.tap`.
        """
        handle = await self.J(selector)
        if not handle:
            raise PageError('No node found for selector: ' + selector)
        await handle.tap()
        await handle.dispose()

    async def type(self, selector: str, text: str, delay: float = 0) -> None:
        """Type ``text`` on the element which matches ``selector``.

        Details see :meth:`pyppeteer.page.Page.type`.
        """
        handle = await self.querySelector(selector)
        if handle is None:
            raise PageError(f'Cannot find {selector} on this page')
        await handle.type(text, delay)
        await handle.dispose()

    def waitFor(
        self, selectorOrFunctionOrTimeout: Union[str, int, float], *args: JSFunctionArg, **kwargs: Any
    ) -> Awaitable[Optional[JSHandle]]:
        """Wait until `selectorOrFunctionOrTimeout`.

        Details see :meth:`pyppeteer.page.Page.waitFor`.
        """
        xPathPattern = '//'
        if helpers.is_js_func(selectorOrFunctionOrTimeout):
            return self.waitForFunction(selectorOrFunctionOrTimeout, *args, **kwargs)
        if isinstance(selectorOrFunctionOrTimeout, str):
            string = selectorOrFunctionOrTimeout
            if string.startswith(xPathPattern):
                return self.waitForXPath(string, **kwargs)
            return self.waitForSelector(string, **kwargs)
        if isinstance(selectorOrFunctionOrTimeout, (int, float)):
            return self._client.loop.create_task(asyncio.sleep(selectorOrFunctionOrTimeout / 1000))
        f = self._client.loop.create_future()
        f.set_exception(BrowserError(f'Unsupported target type: {type(selectorOrFunctionOrTimeout)}'))
        return f

    async def waitForSelector(
        self, selector: str, visible: bool = False, hidden: bool = False, timeout: float = None
    ) -> Optional['ElementHandle']:
        """Wait until element which matches ``selector`` appears on page.

        Details see :meth:`pyppeteer.page.Page.waitForSelector`.
        """
        handle = await self._secondaryWorld.waitForSelector(selector, visible=visible, hidden=hidden, timeout=timeout)
        if handle:
            mainExecutionContext = await self._mainWorld.executionContext
            result = await mainExecutionContext._adoptElementHandle(handle)
            await handle.dispose()
            return result

    async def waitForXPath(
        self, xpath: str, visible: bool = False, hidden: bool = False, timeout: int = None
    ) -> Optional['ElementHandle']:
        """Wait until element which matches ``xpath`` appears on page.

        Details see :meth:`pyppeteer.page.Page.waitForXPath`.
        """
        handle = await self._secondaryWorld.waitForXpath(xpath, visible=visible, hidden=hidden, timeout=timeout)
        if not handle:
            return None
        mainExecutionContext = await self._mainWorld.executionContext
        result = await mainExecutionContext._adoptElementHandle(handle)
        await handle.dispose()
        return result

    def _navigated(self, framePayload: dict) -> None:
        self._name = framePayload.get('name', '')
        self._navigationURL = framePayload.get('url', '')
        self._url = framePayload.get('url', '')

    def _navigatedWithinDocument(self, url: str) -> None:
        self._url = url

    def _onLifecycleEvent(self, loaderId: str, name: str) -> None:
        if name == 'init':
            self._loaderId = loaderId
            self._lifecycleEvents.clear()
        else:
            self._lifecycleEvents.add(name)

    def _onLoadingStopped(self) -> None:
        self._lifecycleEvents.add('DOMContentLoaded')
        self._lifecycleEvents.add('load')

    def _detach(self) -> None:
        self._detached = True
        self.mainWorld._detach()
        self.secondaryWorld._detach()
        if self._parentFrame:
            self._parentFrame._childFrames.remove(self)
        self._parentFrame = None


from biolib.pyppeteer.pyppeteer.frame.frame_manager import FrameManager  # isort:skip
