eidng8/sttbot

View on GitHub
src/Wiki/Templates/MissionList.php

Summary

Maintainability
C
1 day
Test Coverage
<?php
/**
 * Created by PhpStorm.
 * User: JC
 * Date: 2016-11-17
 * Time: 23:52
 */

namespace eidng8\Wiki\Templates;

use eidng8\Wiki\Models\Mission as MissionModel;
use eidng8\Wiki\Models\Skills;
use eidng8\Wiki\WikiBase;

/**
 * Parse all missions
 */
class MissionList extends WikiBase
{
    /**
     * List of all missions, organized by episodes/cadet
     *
     * @var Mission[][]
     */
    protected $list;

    /**
     * @var \eidng8\Wiki\Models\Mission[][][]
     */
    protected $models = null;

    /**
     * Return mission list
     *
     * @param string $what
     *
     * @return array|\eidng8\Wiki\Models\Mission[][][]
     */
    public function get(string $what = null): array
    {
        if ($this->models) {
            return empty($what) ? $this->models : $this->models[$what];
        }

        $this->models = [];
        foreach ($this->list as $type => $episodes) {
            foreach ($episodes as $missions) {
                $tmp = [];
                /* @var Mission $mission */
                foreach ($missions as $mission) {
                    $tmp[] = $mission->get();
                }//end foreach
                $this->models[$type][] = $tmp;
            }//end foreach
        }//end foreach

        return empty($what) ? $this->models : $this->models[$what];
    }//end get()

    /**
     * Iterate through all away team missions and call the given function.
     * The callback is defined as:
     * function(Models\Mission $mission, int $index, int $episode, string $type)
     * - `$mission` is a mission model instance,
     * - `$index` is the index number within the episode
     * - `$episode` is the index of the episode
     * - `$type` can be either `'epissode'` or `'cadet'`
     *
     * @param callable $func
     */
    public function eachAway(callable $func): void
    {
        foreach ($this->list as $type => $episodes) {
            foreach ($episodes as $episode => $missions) {
                foreach ($missions as $index => $mission) {
                    /* @var Mission $mission */
                    $mission = $mission->get();
                    /* @var MissionModel $mission */
                    if (MissionModel::AWAY_TEAM === $mission->type) {
                        call_user_func_array(
                            $func,
                            [$mission, $index, $episode, $type]
                        );
                    }
                }//end foreach
            }//end foreach
        }//end foreach
    }//end eachAway()

    /**
     * Get mission by name
     *
     * @param string $name   Name of the mission, case insensitive
     * @param string $epName Name of episode, case insensitive
     *
     * @return MissionModel|null
     */
    public function byName(string $name, string $epName = null): ?MissionModel
    {
        $search = trim(strtolower($name));
        $epSearch = trim(strtolower($epName));
        foreach ($this->list as $episodes) {
            foreach ($episodes as $missions) {
                foreach ($missions as $mission) {
                    /* @var Mission $mission */
                    $model = $mission->get();
                    if ($search == strtolower($model->name)
                        && (empty($epSearch)
                            || strtolower($epSearch)
                               == strtolower($model->episode))
                    ) {
                        return $model;
                    }
                }//end foreach
            }//end foreach
        }//end foreach
        return null;
    }//end byName()

    /**
     * Fetch all missions from server and process them to models
     *
     * @return Mission[][]
     */
    public function fetch(): array
    {
        $templates = $this->fetchMissionList();
        $templates['episodes'] = $this->fetchTemplates($templates['episodes']);
        $cadets = $templates['cadet'][1];
        $templates['cadet'] = $this->fetchTemplates($templates['cadet']);

        $this->parseEpisodes($templates);
        $this->fetchCadetCrew($cadets, $templates['cadet']);

        return $this->list = $templates;
    }//end fetch()

    /**
     * Export all missions as array
     *
     * @return array
     */
    public function export(): array
    {
        $epi = 0;
        $episodes = [];
        $missions = [];
        $this->each(
            function (MissionModel $mission) use (
                &$episodes,
                &$missions,
                &$epi
            ) {
                $mission = $mission->toArray();

                // extract episode list
                if ($mission['episode'] != end($episodes)) {
                    $episodes[] = $mission['episode'];
                    $epi = count($episodes) - 1;
                }
                $mission['episode'] = $epi;

                // remove redundant data
                unset(
                    $mission['page'],
                    $mission['index'],
                    $mission['locks'],
                    $mission['traits']
                );

                if (MissionModel::SPACE_BATTLE === $mission['type']) {
                    $missions[] = $mission;
                    return;
                }

                // flatten steps array
                $this->flattenSteps($mission);
                $missions[] = $mission;
            }
        );

        return [$episodes, $missions];
    }//end export()

    /**
     * Iterate through all missions and call the given function.
     * The callback is defined as:
     * function(Models\Mission $mission, int $index, int $episode, string $type)
     * - `$mission` is a mission model instance,
     * - `$index` is the index number within the episode
     * - `$episode` is the index of the episode
     * - `$type` can be either `'epissode'` or `'cadet'`
     *
     * @param callable $func
     */
    public function each(callable $func): void
    {
        foreach ($this->list as $type => $episodes) {
            foreach ($episodes as $episode => $missions) {
                foreach ($missions as $index => $mission) {
                    /* @var Mission $mission */
                    call_user_func_array(
                        $func,
                        [$mission->get(), $index, $episode, $type]
                    );
                }//end foreach
            }//end foreach
        }//end foreach
    }//end each()

    /**
     * Fetch the list through API
     *
     * @return string[][]
     */
    private function fetchMissionList(): array
    {
        $this->parse->resetOptions();
        $this->parse->page('Missions');
        $content = $this->parse->get(true)['wikitext']['*'];
        $offset = strpos($content, 'Cadet Challenges');

        preg_match_all(
            '/^\{\{:(.+)}}$/um',
            substr($content, 0, $offset),
            $episodes
        );

        preg_match_all(
            '/^\{\{:(.+)}}$/um',
            $content,
            $cadet,
            PREG_PATTERN_ORDER,
            $offset
        );

        return compact('episodes', 'cadet');
    }//end fetchMissionList()

    /**
     * Fetch templates through API
     *
     * @param string[][] $matches
     *
     * @return array string[]
     */
    private function fetchTemplates(array $matches): array
    {
        $text = $this->expandTemplates->get(implode('', $matches[0]), true);
        $starts = [];
        $offset = 0;
        foreach ($matches[1] as &$template) {
            $template = str_replace('_', ' ', $template);
            $regex = "/===[\\s\\[]*{$template}[\\s\\]]*===/ui";
            preg_match($regex, $text, $match, PREG_OFFSET_CAPTURE, $offset);
            $offset = $match[0][1];
            $starts[] = $offset;
        }//end foreach

        $count = count($starts) - 1;
        foreach ($matches[1] as $idx => &$template) {
            if ($idx >= $count) {
                $template = substr($text, $starts[$idx]);
            } else {
                $template = substr(
                    $text,
                    $starts[$idx],
                    $starts[$idx + 1] - $starts[$idx]
                );
            }
        }//end foreach

        return $matches[1];
    }//end fetchTemplates()

    /**
     * Parse all episodes
     *
     * @param array $episodes
     */
    private function parseEpisodes(&$episodes): void
    {
        foreach ($episodes as &$items) {
            foreach ($items as &$episode) {
                $episode = $this->parseEpisode($episode);
            }//end foreach
        }//end foreach
    }//end parseEpisodes()

    /**
     * Parse a episode wiki text
     *
     * @param string $episode
     *
     * @return Mission[]
     */
    private function parseEpisode(string $episode): array
    {
        $regex = '/\'\'\'Mission (\d+)(\w?)\'\'\'[^\[]+\[\[([^\]]+)]]/iu';
        preg_match_all($regex, $episode, $missions);
        $missions = array_map(
            function ($mission) {
                $this->parse->page($mission);

                return $this->parse->get(true)['wikitext']['*'];
            },
            $missions[3]
        );

        preg_match('/^=+\[+([^\[\]]+)]+=+/', $episode, $matches);

        return $this->parseMissions($missions, $matches[1] ?? '');
    }//end parseEpisode()

    /**
     * Process missions wiki text
     *
     * @param array  $missions
     * @param string $episode
     *
     * @return array|Mission[]
     */
    private function parseMissions(array $missions, string $episode): array
    {
        $info = [];
        $adv = 'adv:' == strtolower(substr($episode, 0, 4));
        foreach ($missions as $mission) {
            /* @var Mission $miss */
            $miss = Mission::load($mission, null, ['advanced' => $adv]);
            if ($miss) {
                if ($adv) {
                    $miss->get()->episode = $episode;
                }
                $info[] = $miss;
            }
        }//end foreach
        $this->fetchImages($info);
        return $info;
    }//end parseMissions()

    /**
     * Fetch all cadet mission eligible crew
     *
     * @param array $names
     * @param array $cadet
     */
    private function fetchCadetCrew(array $names, array $cadet)
    {
        foreach ($cadet as $episode => $missions) {
            $this->parse->page($names[$episode], 2);
            preg_match_all(
                '/{{NamePic\|([^}]+)}}/imsu',
                $this->parse->get(true)['wikitext']['*'],
                $text
            );

            $eligible = $text[1] ?? null;
            foreach ($missions as $mission) {
                /* @var Mission $mission */
                foreach ($mission->get()->steps as $step) {
                    $step['eligible'] = $eligible;
                }//end foreach
            }//end foreach
        }//end foreach
    }//end fetchCadetCrew()

    /**
     * Flatten mission steps array
     *
     * @param array $mission
     */
    private function flattenSteps(array &$mission): void
    {
        foreach ($mission['steps'] as &$step) {
            if (empty($step['locks'])) {
                unset($step['locks']);
            }

            // flatten skills array
            $skills = [];
            foreach ($step['skills'] as $idx => $skill) {
                $skills = array_merge(
                    $skills,
                    Skills::skillNames($skill['names'])
                );
                if (!empty($skill['values'])) {
                    $step['req'][$idx] = $skill['values'];
                }
            }//end foreach
            $step['skills'] = $skills;

            // flatten traits array
            $traits = [];
            foreach ($step['traits'] as $idx => $trait) {
                $traits[$idx] = $trait['names'] ?? null;
                if (!empty($trait['values'])) {
                    $step['bonus'][$idx] = $trait['values'];
                }
            }//end foreach
            $step['traits'] = $traits;
        }//end foreach
    }//end flattenSteps()

    /**
     * Fetch all image URL
     *
     * @param Mission[] $missions
     *
     * @return void
     */
    private function fetchImages(array $missions): void
    {
        foreach ($missions as $mission) {
            $mission = $mission->get();
            $mission->image = $this->query->imageInfo(
                ["File:{$mission->image['file']}" => $mission->name],
                [
                    $mission->image['size'],
                    round($mission->image['size'] * 1.5),
                    $mission->image['size'] * 2,
                ]
            )[$mission->name];

            if (MissionModel::AWAY_TEAM != $mission->type) {
                continue;
            }
            foreach ($mission->steps as $step) {
                foreach ($step->images as $idx => &$image) {
                    if (empty($image)) {
                        continue;
                    }
                    $image = $this->query->imageInfo(
                        ["File:AT-$image.png" => $step->alt[$idx]]
                    )[$step->alt[$idx]];
                }//end foreach
            }//end foreach
        }//end foreach
    }//end validateModel()
}//end class