001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2024
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas/
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see https://www.gnu.org/licenses/.
027 */
028
029package edu.wisc.ssec.mcidasv.startupmanager.options;
030
031import java.io.BufferedReader;
032import java.io.BufferedWriter;
033import java.io.File;
034import java.io.FileNotFoundException;
035import java.io.FileReader;
036import java.io.FileWriter;
037import java.io.IOException;
038import java.util.ArrayList;
039import java.util.Collection;
040import java.util.Collections;
041import java.util.HashMap;
042import java.util.List;
043import java.util.Map;
044import java.util.stream.Collectors;
045
046import edu.wisc.ssec.mcidasv.startupmanager.StartupManager;
047import edu.wisc.ssec.mcidasv.startupmanager.Platform;
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051public class OptionMaster {
052    
053    private static final Logger logger =
054        LoggerFactory.getLogger(OptionMaster.class);
055    
056    public final static String SET_PREFIX = "SET ";
057    public final static String EMPTY_STRING = "";
058    public final static String QUOTE_STRING = "\"";
059    public final static char QUOTE_CHAR = '"';
060
061    // TODO(jon): write CollectionHelpers.zip() and CollectionHelpers.zipWith()
062    public final Object[][] blahblah = {
063        // Default memory initial setting is 80% of system memory
064        { "HEAP_SIZE", "Memory", "80P", Type.MEMORY, OptionPlatform.ALL, Visibility.VISIBLE },
065        { "JOGL_TOGL", "Enable JOGL", "1", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE },
066        { "USE_3DSTUFF", "Enable 3D controls", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
067        { "DEFAULT_LAYOUT", "Load default layout", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
068        { "STARTUP_BUNDLE", "Defaults", "0;", Type.FILE, OptionPlatform.ALL, Visibility.VISIBLE },
069        // mcidasv enables this (the actual property is "visad.java3d.geometryByRef")
070        // by default in mcidasv.properties.
071        { "USE_GEOBYREF", "Enable access to geometry by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
072        { "USE_IMAGEBYREF", "Enable access to image data by reference", "1", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
073        { "USE_NPOT", "Enable Non-Power of Two (NPOT) textures", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
074        // temp bandaid for people suffering from permgen problems.
075        { "USE_CMSGC", "Enable concurrent mark-sweep garbage collector", "0", Type.BOOLEAN, OptionPlatform.ALL, Visibility.VISIBLE },
076        { "LOG_LEVEL", "Log Level", "INFO", Type.LOGLEVEL, OptionPlatform.ALL, Visibility.VISIBLE },
077        { "JVM_OPTIONS", "Java Virtual Machine Options", "", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
078        { "TEXTURE_WIDTH", "Texture Size", "4096", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
079        { "MCV_SCALING", "GUI Scaling", "1", Type.TEXT, OptionPlatform.ALL, Visibility.VISIBLE },
080//        { "USE_DARK_MODE", "Enable Dark Mode", "0", Type.BOOLEAN, OptionPlatform.UNIXLIKE, Visibility.VISIBLE },
081        { "USE_DARK_MODE", "Enable Dark Mode", "0", Type.BOOLEAN, OptionPlatform.MAC, Visibility.VISIBLE },
082    };
083    
084    /**
085     * {@link Option}s can be either platform-specific or applicable to all
086     * platforms. Options that are platform-specific still appear in the 
087     * UI, but their component is not enabled.
088     */
089    public enum OptionPlatform { ALL, UNIXLIKE, WINDOWS, MAC };
090    
091    /**
092     * The different types of {@link Option}s.
093     * 
094     * @see TextOption
095     * @see BooleanOption
096     * @see MemoryOption
097     * @see DirectoryOption
098     * @see SliderOption
099     * @see LoggerLevelOption
100     * @see FileOption
101     */
102    public enum Type { TEXT, BOOLEAN, MEMORY, DIRTREE, SLIDER, LOGLEVEL, FILE };
103    
104    /** 
105     * Different ways that an {@link Option} might be displayed.
106     */
107    public enum Visibility { VISIBLE, HIDDEN };
108    
109    /** Maps an option ID to the corresponding object. */
110    private Map<String, ? extends Option> optionMap;
111    
112    private static OptionMaster instance;
113    
114    public OptionMaster() {
115        normalizeUserDirectory();
116        optionMap = buildOptions(blahblah);
117//        readStartup();
118    }
119    
120    public static OptionMaster getInstance() {
121        if (instance == null) {
122            instance = new OptionMaster();
123        }
124        return instance;
125    }
126    
127    /**
128     * Creates the specified options and returns a mapping of the option ID
129     * to the actual {@link Option} object.
130     * 
131     * @param options An array specifying the {@code Option}s to be built.
132     * 
133     * @return Mapping of ID to {@code Option}.
134     * 
135     * @throws AssertionError if the option array contained an entry that
136     * this method cannot build.
137     */
138    private Map<String, Option> buildOptions(final Object[][] options) {
139        // TODO(jon): seriously, get that zip stuff working! this array 
140        // stuff is BAD.
141        Map<String, Option> optMap = new HashMap<>(options.length);
142        
143        for (Object[] arrayOption : options) {
144            String id = (String)arrayOption[0];
145            String label = (String)arrayOption[1];
146            String defaultValue = (String)arrayOption[2];
147            Type type = (Type)arrayOption[3];
148            OptionPlatform platform = (OptionPlatform)arrayOption[4];
149            Visibility visibility = (Visibility)arrayOption[5];
150            
151            switch (type) {
152                case TEXT:
153                    optMap.put(id, new TextOption(id, label, defaultValue, platform, visibility));
154                    break;
155                case BOOLEAN:
156                    optMap.put(id, new BooleanOption(id, label, defaultValue, platform, visibility));
157                    break;
158                case MEMORY:
159                    optMap.put(id, new MemoryOption(id, label, defaultValue, platform, visibility));
160                    break;
161                case DIRTREE:
162                    optMap.put(id, new DirectoryOption(id, label, defaultValue, platform, visibility));
163                    break;
164                case SLIDER:
165                    optMap.put(id, new SliderOption(id, label, defaultValue, platform, visibility));
166                    break;
167                case LOGLEVEL:
168                    optMap.put(id, new LoggerLevelOption(id, label, defaultValue, platform, visibility));
169                    break;
170                case FILE:
171                    optMap.put(id, new FileOption(id, label, defaultValue, platform, visibility));
172                    break;
173                default:
174                     throw new AssertionError(type + 
175                         " is not known to OptionMaster.buildOptions()");
176            }
177        }
178        return optMap;
179    }
180    
181    /**
182     * Converts a {@link Platform} to its corresponding 
183     * {@link OptionPlatform} type.
184     * 
185     * @return The current platform as a {@code OptionPlatform} type.
186     * 
187     * @throws AssertionError if {@link StartupManager#getPlatform()} 
188     * returned something that this method cannot convert.
189     */
190    // a lame-o hack :(
191    protected OptionPlatform convertToOptionPlatform() {
192        Platform platform = StartupManager.getInstance().getPlatform();
193        switch (platform) {
194            case WINDOWS: 
195                return OptionPlatform.WINDOWS;
196            case MAC:
197                return OptionPlatform.MAC;
198            case UNIXLIKE: 
199                return OptionPlatform.UNIXLIKE;
200            default: 
201                throw new AssertionError("Unknown platform: " + platform);
202        }
203    }
204    
205    /**
206     * Returns the {@link Option} mapped to {@code id}.
207     * 
208     * @param id The ID whose associated {@code Option} is to be returned.
209     * 
210     * @return Either the {@code Option} associated with {@code id}, or 
211     * {@code null} if there was no association.
212     * 
213     * 
214     * @see #getMemoryOption
215     * @see #getBooleanOption
216     * @see #getDirectoryOption
217     * @see #getSliderOption
218     * @see #getTextOption
219     * @see #getLoggerLevelOption
220     * @see #getFileOption
221     */
222    private Option getOption(final String id) {
223        return optionMap.get(id);
224    }
225    
226    /**
227     * Searches {@link #optionMap} for the {@link MemoryOption} that 
228     * corresponds with the given {@code id}.
229     * 
230     * @param id Identifier for the desired {@code MemoryOption}. 
231     * Should not be {@code null}.
232     * 
233     * @return Either the {@code MemoryOption} that corresponds to {@code id} 
234     * or {@code null}.
235     */
236    public MemoryOption getMemoryOption(final String id) {
237        return (MemoryOption)optionMap.get(id);
238    }
239    
240    /**
241     * Searches {@link #optionMap} for the {@link BooleanOption} that 
242     * corresponds with the given {@code id}.
243     * 
244     * @param id Identifier for the desired {@code BooleanOption}. 
245     * Should not be {@code null}.
246     * 
247     * @return Either the {@code BooleanOption} that corresponds to {@code id} 
248     * or {@code null}.
249     */
250    public BooleanOption getBooleanOption(final String id) {
251        return (BooleanOption)optionMap.get(id);
252    }
253    
254    /**
255     * Searches {@link #optionMap} for the {@link DirectoryOption} that 
256     * corresponds with the given {@code id}.
257     * 
258     * @param id Identifier for the desired {@code DirectoryOption}. 
259     * Should not be {@code null}.
260     * 
261     * @return Either the {@code DirectoryOption} that corresponds to 
262     * {@code id} or {@code null}.
263     */
264    public DirectoryOption getDirectoryOption(final String id) {
265        return (DirectoryOption)optionMap.get(id);
266    }
267    
268    /**
269     * Searches {@link #optionMap} for the {@link SliderOption} that 
270     * corresponds with the given {@code id}.
271     * 
272     * @param id Identifier for the desired {@code SliderOption}. 
273     * Should not be {@code null}.
274     * 
275     * @return Either the {@code SliderOption} that corresponds to {@code id} 
276     * or {@code null}.
277     */
278    public SliderOption getSliderOption(final String id) {
279        return (SliderOption)optionMap.get(id);
280    }
281    
282    /**
283     * Searches {@link #optionMap} for the {@link TextOption} that 
284     * corresponds with the given {@code id}.
285     * 
286     * @param id Identifier for the desired {@code TextOption}. 
287     * Should not be {@code null}.
288     * 
289     * @return Either the {@code TextOption} that corresponds to {@code id} 
290     * or {@code null}.
291     */
292    public TextOption getTextOption(final String id) {
293        return (TextOption)optionMap.get(id);
294    }
295    
296    /**
297     * Searches {@link #optionMap} for the {@link LoggerLevelOption} that 
298     * corresponds with the given {@code id}.
299     * 
300     * @param id Identifier for the desired {@code LoggerLevelOption}. 
301     * Should not be {@code null}.
302     * 
303     * @return Either the {@code LoggerLevelOption} that corresponds to {@code id} 
304     * or {@code null}.
305     */
306    public LoggerLevelOption getLoggerLevelOption(final String id) {
307        return (LoggerLevelOption)optionMap.get(id);
308    }
309
310    public FileOption getFileOption(final String id) {
311        return (FileOption)optionMap.get(id);
312    }
313
314    // TODO(jon): getAllOptions and optionsBy* really need some work.
315    // I want to eventually do something like:
316    // Collection<Option> = getOpts().byPlatform(WINDOWS, ALL).byType(BOOLEAN).byVis(HIDDEN)
317    /**
318     * Returns all the available startup manager options.
319     * 
320     * @return Either all available startup manager options or an empty 
321     * {@link Collection}.
322     */
323    public Collection<Option> getAllOptions() {
324        return Collections.unmodifiableCollection(optionMap.values());
325    }
326    
327    /**
328     * Returns the {@link Option Options} applicable to the given 
329     * {@link OptionPlatform OptionPlatforms}.
330     * 
331     * @param platforms Desired platforms. Cannot be {@code null}.
332     * 
333     * @return Either a {@link List} of {code Option}-s applicable to
334     * {@code platforms} or an empty {@code List}.
335     */
336    public List<Option> optionsByPlatform(
337        final Collection<OptionPlatform> platforms) 
338    {
339        if (platforms == null) {
340            throw new NullPointerException("must specify platforms");
341        }
342        Collection<Option> allOptions = getAllOptions();
343        List<Option> filteredOptions = 
344            new ArrayList<>(allOptions.size());
345
346        filteredOptions.addAll(
347            allOptions.stream()
348                      .filter(option ->
349                              platforms.contains(option.getOptionPlatform()))
350                      .collect(Collectors.toList()));
351
352        return filteredOptions;
353    }
354    
355    /**
356     * Returns the {@link Option Options} that match the given 
357     * {@link Type Types}. 
358     * 
359     * @param types Desired {@code Option} types. Cannot be {@code null}.
360     * 
361     * @return Either the {@code List} of {@code Option}-s that match the given 
362     * types or an empty {@code List}.
363     */
364    public List<Option> optionsByType(final Collection<Type> types) {
365        if (types == null) {
366            throw new NullPointerException("must specify types");
367        }
368        Collection<Option> allOptions = getAllOptions();
369        List<Option> filteredOptions = 
370            new ArrayList<>(allOptions.size());
371        filteredOptions.addAll(
372            allOptions.stream()
373                      .filter(option -> types.contains(option.getOptionType()))
374                      .collect(Collectors.toList()));
375        return filteredOptions;
376    }
377    
378    /**
379     * Returns the {@link Option Options} that match the given levels of 
380     * {@link Visibility visibility}.
381     * 
382     * @param visibilities Desired visibility levels. Cannot be {@code null}.
383     * 
384     * @return Either the {@code List} of {@code Option}-s that match the given 
385     * visibility levels or an empty {@code List}. 
386     */
387    public List<Option> optionsByVisibility(
388        final Collection<Visibility> visibilities) 
389    {
390        if (visibilities == null) {
391            throw new NullPointerException("must specify visibilities");
392        }
393        Collection<Option> allOptions = getAllOptions();
394        List<Option> filteredOptions = 
395            new ArrayList<>(allOptions.size());
396        filteredOptions.addAll(
397            allOptions.stream()
398                      .filter(option ->
399                              visibilities.contains(option.getOptionVisibility()))
400                      .collect(Collectors.toList()));
401        return filteredOptions;
402    }
403    
404    public void normalizeUserDirectory() {
405        StartupManager startup = StartupManager.getInstance();
406        Platform platform = startup.getPlatform();
407        File dir = new File(platform.getUserDirectory());
408        File prefs = new File(platform.getUserPrefs());
409        
410        if (!dir.exists()) {
411            dir.mkdir();
412        }
413        if (!prefs.exists()) {
414            try {
415                File defaultPrefs = new File(platform.getDefaultPrefs());
416                startup.copy(defaultPrefs, prefs);
417            } catch (IOException e) {
418                System.err.println("Non-fatal error copying user preference template: "+e.getMessage());
419            }
420        }
421    }
422    
423    public void readStartup() {
424        File script =
425            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
426        try (BufferedReader br = new BufferedReader(new FileReader(script))) {
427            String line;
428            while ((line = br.readLine()) != null) {
429                if (line.startsWith("#")) {
430                    continue;
431                }
432                int splitAt = line.indexOf('=');
433                if (splitAt >= 0) {
434                    String id = line.substring(0, splitAt).replace(SET_PREFIX, EMPTY_STRING);
435                    Option option = getOption(id);
436                    if (option != null) {
437                        System.err.println("setting '"+id+"' with '"+line+'\'');
438                        option.fromPrefsFormat(line);
439                    } else {
440                        System.err.println("Warning: Unknown ID '"+id+'\'');
441                    }
442                } else {
443                    System.err.println("Warning: Bad line format '"+line+'\'');
444                }
445            }
446        } catch (IOException e) {
447            System.err.println("Non-fatal error reading the user preferences: "+e.getMessage());
448        }
449    }
450    
451    public void writeStartup() {
452        File script = 
453            new File(StartupManager.getInstance().getPlatform().getUserPrefs());
454        if (script.getPath().isEmpty()) {
455            return;
456        }
457        // TODO(jon): use filters when you've made 'em less stupid
458        String newLine = 
459                StartupManager.getInstance().getPlatform().getNewLine();
460        OptionPlatform currentPlatform = convertToOptionPlatform();
461        StringBuilder contents = new StringBuilder(2048);
462        for (Object[] arrayOption : blahblah) {
463            Option option = getOption((String)arrayOption[0]);
464            OptionPlatform platform = option.getOptionPlatform();
465            if ((platform == OptionPlatform.ALL) || (platform == currentPlatform)) {
466                contents.append(option.toPrefsFormat()).append(newLine);
467            }
468        }
469        
470        try (BufferedWriter out = new BufferedWriter(new FileWriter(script))) {
471            out.write(contents.toString());
472        } catch (IOException e) {
473            logger.error("Could not write to McIDAS-V startup prefs file", e);
474        }
475    }
476}