<commit_before>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import UIKit
import SnapKit
import Shared

struct IntroUX {
    static let Width = 375
    static let Height = 667
    static let MinimumFontScale: CGFloat = 0.5
    static let PagerCenterOffsetFromScrollViewBottom = UIScreen.main.bounds.width <= 320 ? 20 : 30
    static let StartBrowsingButtonColor = UIColor.Defaults.Blue40
    static let StartBrowsingButtonHeight = 56
    static let SignInButtonColor = UIColor.Defaults.Blue40
    static let SignInButtonHeight = 60
    static let PageControlHeight = 40
    static let SignInButtonWidth = 290
    static let CardTextWidth = UIScreen.main.bounds.width <= 320 ? 240 : 280
    static let FadeDuration = 0.25
}

protocol IntroViewControllerDelegate: class {
    func introViewControllerDidFinish(_ introViewController: IntroViewController, requestToLogin: Bool)
}

class IntroViewController: UIViewController {
    weak var delegate: IntroViewControllerDelegate?

    // We need to hang on to views so we can animate and change constraints as we scroll
    var cardViews = [CardView]()
    var cards = IntroCard.defaultCards()

    lazy fileprivate var startBrowsingButton: UIButton = {
        let button = UIButton()
        button.backgroundColor = UIColor.clear
        button.setTitle(Strings.StartBrowsingButtonTitle, for: UIControlState())
        button.setTitleColor(IntroUX.StartBrowsingButtonColor, for: UIControlState())
        button.addTarget(self, action: #selector(IntroViewController.startBrowsing), for: UIControlEvents.touchUpInside)
        button.accessibilityIdentifier = "IntroViewController.startBrowsingButton"
        return button
    }()

    lazy var pageControl: UIPageControl = {
        let pc = UIPageControl()
        pc.pageIndicatorTintColor = UIColor.black.withAlphaComponent(0.3)
        pc.currentPageIndicatorTintColor = UIColor.black
        pc.accessibilityIdentifier = "IntroViewController.pageControl"
        pc.addTarget(self, action: #selector(IntroViewController.changePage), for: UIControlEvents.valueChanged)
        return pc
    }()

    lazy fileprivate var scrollView: UIScrollView = {
        let sc = UIScrollView()
        sc.backgroundColor = UIColor.clear
        sc.accessibilityLabel = NSLocalizedString("Intro Tour Carousel", comment: "Accessibility label for the introduction tour carousel")
        sc.delegate = self
        sc.bounces = false
        sc.isPagingEnabled = true
        sc.showsHorizontalScrollIndicator = false
        sc.accessibilityIdentifier = "IntroViewController.scrollView"
        return sc
    }()

    var horizontalPadding: Int {
        return self.view.frame.width <= 320 ? 20 : 50
    }

    var verticalPadding: CGFloat {
        return self.view.frame.width <= 320 ? 10 : 38
    }

    lazy fileprivate var imageViewContainer: UIStackView = {
        let sv = UIStackView()
        sv.axis = .horizontal
        sv.distribution = .fillEqually
        return sv
    }()

    // Because a stackview cannot have a background color
    fileprivate var imagesBackgroundView = UIView()

    override func viewDidLoad() {
        if AppConstants.MOZ_LP_INTRO {
            syncViaLP()
        }

        assert(cards.count > 1, "Intro is empty. At least 2 cards are required")
        view.backgroundColor = UIColor.white

        // Add Views
        view.addSubview(pageControl)
        view.addSubview(scrollView)
        view.addSubview(startBrowsingButton)
        scrollView.addSubview(imagesBackgroundView)
        scrollView.addSubview(imageViewContainer)

        // Setup constraints
        imagesBackgroundView.snp.makeConstraints { make in
            make.edges.equalTo(imageViewContainer)
        }
        imageViewContainer.snp.makeConstraints { make in
            make.top.equalTo(self.view)
            make.height.equalTo(self.view.snp.width)
        }
        startBrowsingButton.snp.makeConstraints { make in
            make.left.right.equalTo(self.view)
            make.bottom.equalTo(self.view.safeArea.bottom)
            make.height.equalTo(IntroUX.StartBrowsingButtonHeight)
        }
        scrollView.snp.makeConstraints { make in
            make.left.right.top.equalTo(self.view)
            make.bottom.equalTo(startBrowsingButton.snp.top)
        }
      
        pageControl.snp.makeConstraints { make in
            make.centerX.equalTo(self.scrollView)
            make.centerY.equalTo(self.startBrowsingButton.snp.top).offset(-IntroUX.PagerCenterOffsetFromScrollViewBottom)
        }

        createSlides()
        pageControl.addTarget(self, action: #selector(changePage), for: .valueChanged)
    }

    func syncViaLP() {
        let startTime = Date.now()
        LeanPlumClient.shared.introScreenVars?.onValueChanged({ [weak self] in
            guard let newIntro = LeanPlumClient.shared.introScreenVars?.object(forKey: nil) as? [[String: Any]] else {
                return
            }
            let decoder = JSONDecoder()
            let newCards = newIntro.flatMap { (obj) -> IntroCard? in
                guard let object = try? JSONSerialization.data(withJSONObject: obj, options: []) else {
                    return nil
                }
                let card = try? decoder.decode(IntroCard.self, from: object)
                // Make sure the selector actually goes somewhere. Otherwise dont show that slide
                if let selectorString = card?.buttonSelector, let wself = self {
                    return wself.responds(to: NSSelectorFromString(selectorString)) ? card : nil
                } else {
                    return card
                }
            }

            guard newCards != IntroCard.defaultCards(), newCards.count > 1 else {
                return
            }

            // We need to still be on the first page otherwise the content will change underneath the user's finger
            // We also need to let LP know this happened so we can track when a A/B test was not run
            guard self?.pageControl.currentPage == 0 else {
                let totalTime = Date.now() - startTime
                LeanPlumClient.shared.track(event: .onboardingTestLoadedTooSlow, withParameters: ["Total time": "\(totalTime) ms" as AnyObject])
                return
            }

            self?.cards = newCards
            self?.createSlides()
            self?.viewDidLayoutSubviews()

        })
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.contentSize = imageViewContainer.frame.size
    }

    func createSlides() {
        // Wipe any existing slides
        imageViewContainer.subviews.forEach { $0.removeFromSuperview() }
        cardViews.forEach { $0.removeFromSuperview() }
        cardViews = cards.flatMap { addIntro(card: $0) }
        pageControl.numberOfPages = cardViews.count
        setupDynamicFonts()
        if let firstCard = cardViews.first {
            setActive(firstCard, forPage: 0)
        }
        imageViewContainer.layoutSubviews()
        scrollView.contentSize = imageViewContainer.frame.size
    }

    func addIntro(card: IntroCard) -> CardView? {
        guard let image = UIImage(named: card.imageName) else {
            return nil
        }
        let imageView = UIImageView(image: image)
        imageViewContainer.addArrangedSubview(imageView)
        imageView.snp.makeConstraints { make in
            make.height.equalTo(imageViewContainer.snp.height)
            make.width.equalTo(imageViewContainer.snp.height)
        }
<commit_msg>Bug 1437500 - Make sure that the Intro controller does its initial setup before setting up slides. (#3654)<commit_after>/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

import UIKit
import SnapKit
import Shared

struct IntroUX {
    static let Width = 375
    static let Height = 667
    static let MinimumFontScale: CGFloat = 0.5
    static let PagerCenterOffsetFromScrollViewBottom = UIScreen.main.bounds.width <= 320 ? 20 : 30
    static let StartBrowsingButtonColor = UIColor.Defaults.Blue40
    static let StartBrowsingButtonHeight = 56
    static let SignInButtonColor = UIColor.Defaults.Blue40
    static let SignInButtonHeight = 60
    static let PageControlHeight = 40
    static let SignInButtonWidth = 290
    static let CardTextWidth = UIScreen.main.bounds.width <= 320 ? 240 : 280
    static let FadeDuration = 0.25
}

protocol IntroViewControllerDelegate: class {
    func introViewControllerDidFinish(_ introViewController: IntroViewController, requestToLogin: Bool)
}

class IntroViewController: UIViewController {
    weak var delegate: IntroViewControllerDelegate?

    // We need to hang on to views so we can animate and change constraints as we scroll
    var cardViews = [CardView]()
    var cards = IntroCard.defaultCards()

    lazy fileprivate var startBrowsingButton: UIButton = {
        let button = UIButton()
        button.backgroundColor = UIColor.clear
        button.setTitle(Strings.StartBrowsingButtonTitle, for: UIControlState())
        button.setTitleColor(IntroUX.StartBrowsingButtonColor, for: UIControlState())
        button.addTarget(self, action: #selector(IntroViewController.startBrowsing), for: UIControlEvents.touchUpInside)
        button.accessibilityIdentifier = "IntroViewController.startBrowsingButton"
        return button
    }()

    lazy var pageControl: UIPageControl = {
        let pc = UIPageControl()
        pc.pageIndicatorTintColor = UIColor.black.withAlphaComponent(0.3)
        pc.currentPageIndicatorTintColor = UIColor.black
        pc.accessibilityIdentifier = "IntroViewController.pageControl"
        pc.addTarget(self, action: #selector(IntroViewController.changePage), for: UIControlEvents.valueChanged)
        return pc
    }()

    lazy fileprivate var scrollView: UIScrollView = {
        let sc = UIScrollView()
        sc.backgroundColor = UIColor.clear
        sc.accessibilityLabel = NSLocalizedString("Intro Tour Carousel", comment: "Accessibility label for the introduction tour carousel")
        sc.delegate = self
        sc.bounces = false
        sc.isPagingEnabled = true
        sc.showsHorizontalScrollIndicator = false
        sc.accessibilityIdentifier = "IntroViewController.scrollView"
        return sc
    }()

    var horizontalPadding: Int {
        return self.view.frame.width <= 320 ? 20 : 50
    }

    var verticalPadding: CGFloat {
        return self.view.frame.width <= 320 ? 10 : 38
    }

    lazy fileprivate var imageViewContainer: UIStackView = {
        let sv = UIStackView()
        sv.axis = .horizontal
        sv.distribution = .fillEqually
        return sv
    }()

    // Because a stackview cannot have a background color
    fileprivate var imagesBackgroundView = UIView()

    override func viewDidLoad() {
        if AppConstants.MOZ_LP_INTRO {
            syncViaLP()
        }

        assert(cards.count > 1, "Intro is empty. At least 2 cards are required")
        view.backgroundColor = UIColor.white

        // Add Views
        view.addSubview(pageControl)
        view.addSubview(scrollView)
        view.addSubview(startBrowsingButton)
        scrollView.addSubview(imagesBackgroundView)
        scrollView.addSubview(imageViewContainer)

        // Setup constraints
        imagesBackgroundView.snp.makeConstraints { make in
            make.edges.equalTo(imageViewContainer)
        }
        imageViewContainer.snp.makeConstraints { make in
            make.top.equalTo(self.view)
            make.height.equalTo(self.view.snp.width)
        }
        startBrowsingButton.snp.makeConstraints { make in
            make.left.right.equalTo(self.view)
            make.bottom.equalTo(self.view.safeArea.bottom)
            make.height.equalTo(IntroUX.StartBrowsingButtonHeight)
        }
        scrollView.snp.makeConstraints { make in
            make.left.right.top.equalTo(self.view)
            make.bottom.equalTo(startBrowsingButton.snp.top)
        }
      
        pageControl.snp.makeConstraints { make in
            make.centerX.equalTo(self.scrollView)
            make.centerY.equalTo(self.startBrowsingButton.snp.top).offset(-IntroUX.PagerCenterOffsetFromScrollViewBottom)
        }

        createSlides()
        pageControl.addTarget(self, action: #selector(changePage), for: .valueChanged)
    }

    func syncViaLP() {
        let startTime = Date.now()
        LeanPlumClient.shared.introScreenVars?.onValueChanged({ [weak self] in
            guard let newIntro = LeanPlumClient.shared.introScreenVars?.object(forKey: nil) as? [[String: Any]] else {
                return
            }
            let decoder = JSONDecoder()
            let newCards = newIntro.flatMap { (obj) -> IntroCard? in
                guard let object = try? JSONSerialization.data(withJSONObject: obj, options: []) else {
                    return nil
                }
                let card = try? decoder.decode(IntroCard.self, from: object)
                // Make sure the selector actually goes somewhere. Otherwise dont show that slide
                if let selectorString = card?.buttonSelector, let wself = self {
                    return wself.responds(to: NSSelectorFromString(selectorString)) ? card : nil
                } else {
                    return card
                }
            }

            guard newCards != IntroCard.defaultCards(), newCards.count > 1 else {
                return
            }

            // We need to still be on the first page otherwise the content will change underneath the user's finger
            // We also need to let LP know this happened so we can track when a A/B test was not run
            guard self?.pageControl.currentPage == 0 else {
                let totalTime = Date.now() - startTime
                LeanPlumClient.shared.track(event: .onboardingTestLoadedTooSlow, withParameters: ["Total time": "\(totalTime) ms" as AnyObject])
                return
            }

            self?.cards = newCards
            self?.createSlides()
            self?.viewDidLayoutSubviews()

        })
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        scrollView.contentSize = imageViewContainer.frame.size
    }

    func createSlides() {
        // Make sure the scrollView has been setup before setting up the slides
        guard scrollView.superview != nil else {
            return
        }
        // Wipe any existing slides
        imageViewContainer.subviews.forEach { $0.removeFromSuperview() }
        cardViews.forEach { $0.removeFromSuperview() }
        cardViews = cards.flatMap { addIntro(card: $0) }
        pageControl.numberOfPages = cardViews.count
        setupDynamicFonts()
        if let firstCard = cardViews.first {
            setActive(firstCard, forPage: 0)
        }
        imageViewContainer.layoutSubviews()
        scrollView.contentSize = imageViewContainer.frame.size
    }

    func addIntro(card: IntroCard) -> CardView? {
        guard let image = UIImage(named: card.imageName) else {
            return nil
        }
        let imageView = UIImageView(image: image)
        imageViewContainer.addArrangedSubview(imageView)
        imageView.snp.makeConstraints { make in
            make.height.equalTo(imageViewContainer.snp.height)
            make.width.equalTo(imageViewContainer.snp.height)
        }

{'fraction_non_alphanumeric': 0.07898934473298029, 'fraction_numerical': 0.009379187137114784, 'mean_word_length': 2.4387889425186486, 'pattern_counts': {'":': 2, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '4884131', 'n_tokens_mistral': 4426, 'n_tokens_neox': 4191, 'n_words': 1055}